Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ class template with conditional data members

Consider the following C++ code.

#include <iostream>
#include <set>
#include <string>

enum class Field { kX, kY };

std::string ToString(const Field f) {
  switch (f) {
  case Field::kX:
    return "x";
  case Field::kY:
    return "y";
  default:
    return "?";
  }
}

std::set<std::string> FieldStrings(const bool has_x, const bool has_y) {
  std::set<std::string> field_strings;
  if (has_x) {
    field_strings.insert(ToString(Field::kX));
  }
  if (has_y) {
    field_strings.insert(ToString(Field::kY));
  }
  return field_strings;
}

template <Field... Args> struct S {
  int x = 0; // Should be present if and only if `kX` in `Args`.
  int y = 0; // Should be present if and only if `kY` in `Args`.

  // Should return `ToString` called on all of the `Field`s in `Args`.
  static const std::set<std::string> Fields() {
    static const std::set<std::string> kFields;
    return kFields;
  }
};

template <bool HasX, bool HasY> struct T {
  // Returns the fields that are available in the struct.
  static const std::set<std::string> &Fields() {
    static const std::set<std::string> kFields;
    return kFields;
  }
};
template <> struct T<false, true> {
  int y = 0;
  static const std::set<std::string> &Fields() {
    static const std::set<std::string> kFields = FieldStrings(false, true);
    return kFields;
  }
};
template <> struct T<true, false> {
  int x = 0;
  static const std::set<std::string> &Fields() {
    static const std::set<std::string> kFields = FieldStrings(true, false);
    return kFields;
  }
};
template <> struct T<true, true> {
  int x = 0;
  int y = 0;
  static const std::set<std::string> &Fields() {
    static const std::set<std::string> kFields = FieldStrings(true, true);
    return kFields;
  }
};

The struct S sketches out what I am hoping to achieve:

  • a class template that takes a parameter pack of enums
  • data members S::x and S::y that conditionally exist based on the contents of the template parameter pack
  • a static function member S::Fields that is obtained by transforming the contents of the template parameter pack

The struct T behaves more like what I am hoping to achieve:

  • the data members T::x and T::y conditionally exist based on the template parameters
  • the static function member T::Fields returns a value that depends on the template parameters

T does not behave exactly like what I want because there are separate bool parameters for the fields rather than a pack of Field enums. More importantly, the implementation of T is not scalable: the number of specializations increases exponentially in the number of fields. It is not too bad to write out all the overloads when there are two fields, but it becomes a huge burden if there are 10 fields.

Is there any way to implement S? The key requirements are:

  • we pass in the fields that we want to support in the struct
  • the data members exist conditionally based on the fields we pass in to the template pack
  • the static function depends on the fields we pass in to the template pack
like image 439
Stirling Avatar asked Jun 23 '26 20:06

Stirling


2 Answers

Specialization of the "Leaf" and inherit from those leaves does the job:

template <Field field> struct FieldStorage;

template <>
struct FieldStorage<Field::kX>
{
    int x = 0;

    static constexpr const char* name = "x"; // ToString switch no longer needed
};

template <>
struct FieldStorage<Field::kY>
{
    int y = 0;

    static constexpr const char* name = "y";
};

template<Field... Fs>
struct S : FieldStorage<Fs>...
{
    static const std::set<std::string>& Fields() {
        static const std::set<std::string> kFields{FieldStorage<Fs>::name...};
        return kFields;
    }
};

Demo

like image 163
Jarod42 Avatar answered Jun 26 '26 10:06

Jarod42


Conditionally existing fields can be implemented via inheritance, and compile-time handling of variable-length argument lists can be achieved using pack expansions:

#include <set>
#include <string>
#include <cstdint>
#include <iostream>

enum class Field : uint8_t {
    kX,
    kY
};

namespace {
    std::string ToString(const Field field) {
        switch (field) {
            case Field::kX: return "x";
            case Field::kY: return "y";
            default:        return "?";
        }
    }
}  // namespace

template<Field FieldName>
struct FieldStorage;

template<>
struct FieldStorage<Field::kX> {
    int x{0};
};
template<>
struct FieldStorage<Field::kY> {
    int y{0};
};

template<Field... FieldsList>
struct S : FieldStorage<FieldsList>... {
    static const std::set<std::string>& Fields() {
        static const std::set<std::string> kFields{ToString(FieldsList)...};
        return kFields;
    }
};

int main() {
    S<Field::kX> SWithXOnly;
    SWithXOnly.x = 1;
    // SWithXOnly.y = 2; // Will produce compilation error.

    S<Field::kY> SWithYOnly;
    SWithYOnly.y = 1;
    // SWithYOnly.x = 2; // Will produce compilation error.

    S<Field::kX, Field::kY> SWithBoth;
    SWithBoth.x = 1;
    SWithBoth.y = 2;

    for (const std::string& field : decltype(SWithBoth)::Fields()) { 
        std::cout << field << "\n";
    }
}

This solution scales linearly with the number of fields, improving upon the exponential approach. Further improvements to scalability or reduction of boilerplate code would likely require heavy use of macros.

like image 37
TheAliceBaskerville Avatar answered Jun 26 '26 11:06

TheAliceBaskerville



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!