Created
January 17, 2025 13:26
-
-
Save apirogov/697030d5f7880dc6806804c8da473937 to your computer and use it in GitHub Desktop.
Enum-indexed product types in C++
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// Utilities to implement a type-safe enum-indexed product type. | |
/// | |
/// A product type is the dual of a sum type (std::variant provides a generic sum type in C++). | |
/// | |
/// The canonical product type is std::tuple, however this is not the best choice for a good API. | |
/// | |
/// Any struct can be considered a product type over the member types, but then adding a | |
/// new named case would require boilerplate, such as: | |
/// * adding a new member | |
/// * possibly, adding access methods | |
/// this can be easily forgotten and can lead to mistakes. | |
/// | |
/// Using the mechanism implemented here, whenever a new enum value is added: | |
/// * it is impossible to create an incomplete set of structs | |
/// * it is not necessary to add any new methods or members | |
/// as this is catched at compile-time. | |
#include <iostream> | |
#include <vector> | |
#include <variant> | |
#include <cstddef> | |
namespace indexed_structs | |
{ | |
namespace impl_ | |
{ | |
template <template <typename> typename, typename> | |
struct MapVariant; | |
template <template <typename> typename F, typename... Ts> | |
struct MapVariant<F, std::variant<Ts...>> | |
{ | |
using Result = std::variant<F<Ts>...>; | |
}; | |
} | |
/// Utility template to apply a transformation to the types in a variant. | |
template <typename Variant, template <typename> typename Transform> | |
using MapVariant = typename impl_::MapVariant<Transform, Variant>::Result; | |
/// Helper to map back from index type representing an enum value to the value itself | |
template<typename T> struct FromIxType{ | |
static constexpr auto const value = T::value; | |
}; | |
/// Generic wrapper class to hold a set of indexed structs. | |
template<typename E, template<E> typename D, typename V> | |
class EnumIndexed | |
{ | |
public: | |
using IndexEnum = E; | |
template<E Ix> | |
using Type = D<Ix>; | |
using Variant = V; | |
/// Vector holding one distinct struct for each different enum value. | |
std::vector<V> m_data{}; | |
private: | |
template <std::size_t I = 0> | |
void default_construct() | |
{ | |
if constexpr (I < std::variant_size_v<V>) | |
{ | |
m_data[I] = std::variant_alternative_t<I, V>{}; | |
default_construct<I + 1>(); | |
} | |
} | |
public: | |
EnumIndexed() : m_data(std::vector<V>(std::variant_size_v<V>)) { | |
default_construct(); | |
} | |
template <E Ix> | |
D<Ix> const& get() const | |
{ | |
return std::get<D<Ix>>(m_data.at(static_cast<int>(Ix))); | |
} | |
template <E Ix> | |
void set(D<Ix> const&& obj) | |
{ | |
m_data[static_cast<int>(Ix)] = obj; | |
} | |
}; | |
} | |
#define NEW_ENUM_INDEXED_STRUCT_SET(commonName) \ | |
namespace indexed_structs { \ | |
template<INDEX_ENUM_NAMESPACE::IxEnum T> struct commonName##Dispatch {}; \ | |
} | |
#define ADD_ENUM_INDEXED_STRUCT(commonName, target, specificName) \ | |
namespace indexed_structs { \ | |
template<> struct commonName##Dispatch<target> { using Type = specificName; }; \ | |
} | |
#define END_ENUM_INDEXED_STRUCT_SET(commonName) \ | |
namespace indexed_structs { \ | |
template<INDEX_ENUM_NAMESPACE::IxEnum T> using commonName##FromEnum = typename commonName##Dispatch<T>::Type; \ | |
template<typename T> using commonName##FromLifted = commonName##FromEnum<indexed_structs::FromIxType<T>::value>; \ | |
using commonName##Variant = MapVariant<INDEX_ENUM_NAMESPACE::IxVariant, commonName##FromLifted>; \ | |
} \ | |
using commonName = indexed_structs::EnumIndexed< \ | |
INDEX_ENUM_NAMESPACE::IxEnum, \ | |
indexed_structs::commonName##FromEnum, \ | |
indexed_structs::commonName##Variant \ | |
>; | |
// -------- | |
enum class MyEnum | |
{ | |
X, | |
Y, | |
Z | |
}; | |
struct MySettingsX | |
{ | |
bool foo = true; | |
}; | |
struct MySettingsY | |
{ | |
int bar = 21; | |
}; | |
struct MySettingsZ | |
{}; | |
enum class OtherEnum | |
{ | |
A, | |
B, | |
}; | |
struct StructA | |
{ | |
bool qux = true; | |
void hello() const { | |
std::cout << "StructA " << qux << std::endl; | |
} | |
}; | |
struct StructB | |
{ | |
double blub = 1.23; | |
void hello() const { | |
std::cout << "StructB " << blub << std::endl; | |
} | |
}; | |
template<typename T> | |
void callHello(T const& obj) { | |
obj.hello(); | |
} | |
// -------- | |
// enum lifted to type level (boilerplate, needed once per enum) | |
namespace my_enum { | |
// alias to underlying enum | |
using IxEnum = MyEnum; | |
// type symbols, one per enum value | |
struct X{ static constexpr auto const value = IxEnum::X; }; | |
struct Y{ static constexpr auto const value = IxEnum::Y; }; | |
struct Z{ static constexpr auto const value = IxEnum::Z; }; | |
// variant with all symbolic enum values (simplifies type-level operations) | |
using IxVariant = std::variant<X, Y, Z>; | |
} | |
namespace other_enum { | |
// alias to underlying enum | |
using IxEnum = OtherEnum; | |
// type symbols, one per enum value | |
struct A{ static constexpr auto const value = IxEnum::A; }; | |
struct B{ static constexpr auto const value = IxEnum::B; }; | |
// variant with all symbolic enum values (simplifies type-level operations) | |
using IxVariant = std::variant<A, B>; | |
} | |
// boilerplate macros to create the wrapper type | |
// (once per set of structs indexed by an enum) | |
#define INDEX_ENUM_NAMESPACE my_enum | |
NEW_ENUM_INDEXED_STRUCT_SET(MySettings) | |
ADD_ENUM_INDEXED_STRUCT(MySettings, MyEnum::X, MySettingsX) | |
ADD_ENUM_INDEXED_STRUCT(MySettings, MyEnum::Y, MySettingsY) | |
ADD_ENUM_INDEXED_STRUCT(MySettings, MyEnum::Z, MySettingsZ) | |
END_ENUM_INDEXED_STRUCT_SET(MySettings) | |
#undef INDEX_ENUM_NAMESPACE | |
#define INDEX_ENUM_NAMESPACE other_enum | |
NEW_ENUM_INDEXED_STRUCT_SET(SomeStructs) | |
ADD_ENUM_INDEXED_STRUCT(SomeStructs, OtherEnum::A, StructA) | |
ADD_ENUM_INDEXED_STRUCT(SomeStructs, OtherEnum::B, StructB) | |
END_ENUM_INDEXED_STRUCT_SET(SomeStructs) | |
#undef INDEX_ENUM_NAMESPACE | |
// -------- | |
int main() { | |
auto obj = MySettings(); | |
std::cout << obj.get<MyEnum::X>().foo << std::endl; | |
std::cout << obj.get<MyEnum::Y>().bar << std::endl; | |
obj.set<MyEnum::X>(MySettings::Type<MyEnum::X>{.foo = false}); | |
obj.set<MyEnum::Y>(MySettings::Type<MyEnum::Y>{.bar = 42}); | |
std::cout << obj.get<MyEnum::X>().foo << std::endl; | |
std::cout << obj.get<MyEnum::Y>().bar << std::endl; | |
auto obj2 = SomeStructs(); | |
std::cout << obj2.get<OtherEnum::B>().blub << std::endl; | |
obj2.get<OtherEnum::A>().hello(); | |
obj2.get<OtherEnum::B>().hello(); | |
// TODO: looping pattern? or just unroll by hand, it's good enough... | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment