Skip to content

Instantly share code, notes, and snippets.

@apirogov
Created January 17, 2025 13:26
Show Gist options
  • Save apirogov/697030d5f7880dc6806804c8da473937 to your computer and use it in GitHub Desktop.
Save apirogov/697030d5f7880dc6806804c8da473937 to your computer and use it in GitHub Desktop.
Enum-indexed product types in C++
/// 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