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:
S::x and S::y that conditionally exist based on the contents of the template parameter packS::Fields that is obtained by transforming the contents of the template parameter packThe struct T behaves more like what I am hoping to achieve:
T::x and T::y conditionally exist based on the template parametersT::Fields returns a value that depends on the template parametersT 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:
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With