3

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
3
  • You are not using x or y anywhere. Why do you need them at all? Commented Jul 25 at 7:33
  • 1
    If you're really interested in the latest features of C++ that can do this have a look at reflection. For example watch C++ Reflection Is Not Contemplation - Andrei Alexandrescu - CppCon 2024 Commented Jul 25 at 7:44
  • 1
    For me question explains to much "how" instead "what". I wander what problem this code suppose to solve? I suspect over-engineering. Commented Jul 25 at 8:28

3 Answers 3

8

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

Sign up to request clarification or add additional context in comments.

Comments

5

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.

5 Comments

I like the solution but it can be simplified a lot by just making a template <Field F> struct FieldStorage; class that you specialize for the different Fields. The inheritance then simply becomes struct S : FieldStorage<Fs>... Example
Wouldn't creating an empty tag type template simplify things a bit, or at least make them more concise? E.g.: template<typename = decltype([]{})> struct Empty{}; struct XFieldStorage { int x{0};}; and derivation in the following manner: std::conditional_t< Contains<Field::kX, Fs...>, XFieldStorage, Empty<> >, godbolt.org/z/frvzG9svs
@TheAliceBaskerville Nitpick: It's not a fold expression (available since C++17) but a pack expansion (available since C++11).
Thanks for pointing those out! I definitely need to take more time reviewing what I send here. I really appreciate your help. Would you prefer to post your own answer, or would it be okay if I edit mine to incorporate your simplification?
@TheAliceBaskerville You're welcome! You go ahead and edit if you like the simplification!
0

I think the main question would be how would you like to get your name of fields TheAliceBaskerville provided a great way to calculate fields. I'll suggest just separate out the implementation of data formatting and struct S unless S is used for formatting and get the field names.

Following prints X: x, Y: y, XY:x,y

Live Demo

#include <iostream>
#include <vector>
#include <string>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <algorithm>
#include <iostream>
#include <set>
#include <string>
#include <string_view>
#include <array>

// C++23 has std::to_underlying
template <typename E>
constexpr auto
to_underlying(E e) noexcept
{
    return static_cast<std::underlying_type_t<E>>(e);
}

enum class Field: unsigned { kX, kY, LAST };
 // additional one to set terminate mark
using FieldArray = std::array<Field, to_underlying(Field::LAST) + 1>;

std::string ToString(const Field f) {
  static constexpr std::array<std::string_view,to_underlying(Field::LAST)> STR_MAP{
    "x",
    "y"
  };
  if(f >= Field::LAST){
    return "?";
  }
  return std::string(STR_MAP[to_underlying(f)]);
}

template<typename T>
consteval FieldArray computeFields(){
    std::vector<Field> fields;
    constexpr bool has_x = requires(const T& t) {
        t.x;
    };
    constexpr bool has_y = requires(const T& t) {
        t.y;
    };
    size_t index = 0;
    if(has_x){
        fields.push_back(Field::kX);
    }
    if(has_y){
        fields.push_back(Field::kY);
    }
    FieldArray arr;
    std::copy_n(fields.begin(), fields.size(), arr.begin());
    arr[fields.size()] = Field::LAST;
    
    return arr;
}

template<typename T>
std::set<std::string> FieldStrings() {
  std::set<std::string> field_strings;
  constexpr auto fields = computeFields<T>();
  for(auto field: fields){
    if(field == Field::LAST) break; // End of fields
    field_strings.insert(ToString(field));
  }
  return field_strings;
}

struct DataX{
    int x;

};

struct DataY{
    int y;

};
struct DataXY{
    int x;
    int y;

};

template <typename T> struct S {
  T data{};

  // 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 = FieldStrings<T>();
    return kFields;
  }
};

int main(){ 
    fmt::print("X: {}, Y: {}, XY:{}", 
        fmt::join(S<DataX>::Fields(), ","),
        fmt::join(S<DataY>::Fields(), ","),
        fmt::join(S<DataXY>::Fields(), ",") );
    return 0;
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.