Search code examples
c++design-patternsenumscompilationmacros

Implementing Feature-Based Design with Compile-Time Flexibility in C++


I need feature-based design, where each feature is added to a class's bitmask FeatureList.

However, each class should compile even if the individual features of the class are removed from the compile.

For example, instead of a class looking like this:

// Won't compile if PositionFeature isn't compiled!
#include "PositionFeature.h"

class Object
{
    Object()
    {
        FeatureList.AddFlag(RegisteredTypeIndex<PositionFeature>);
    }
};
ENGINE_REGISTER_TYPE(Object)

It should look like this:

// Should compile without PositionFeature, but do nothing!
class PositionFeature;

class Object
{
    Object()
    {
        FeatureList.AddFlag(RegisteredTypeIndex<PositionFeature>);
    }
};
ENGINE_REGISTER_TYPE(Object)

The issue stems from my current attempt of type registry design, which does not allow FeatureList to correctly search for unregistered features sharing a name with the registered feature. (Currently sees them as different features, and won't compile)

How could this compile-time flexibility be created in c++?

With that asked, this is my current registry:

#pragma once

#include <cstddef>
#include <type_traits>
#include <utility>

template <typename T>
struct tag { using type = T; };

template <typename...>
struct type_list {};

namespace List
{
    // Returns the index of an element in `type_list`, or causes an error if no such element
    template <typename L, typename T>
    struct find_type {};

    template <typename T>
    struct find_type<type_list<>, T> {};
    template <typename F, typename ...P, typename T>
    struct find_type<type_list<F, P...>, T> : std::integral_constant<std::size_t, 1 + find_type<type_list<P...>, T>::value> {};
    template <typename F, typename ...P>
    struct find_type<type_list<F, P...>, F> : std::integral_constant<std::size_t, 0> {};
}

namespace StatefulList
{
    namespace impl
    {
        template <typename Name, std::size_t Index>
        struct ElemReader
        {
        // Hides GCC warnings
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnon-template-friend"
#endif
            friend constexpr auto adl_ListElem(ElemReader<Name, Index>);
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic pop
#endif
        };

        template <typename Name, std::size_t Index, typename Value>
        struct ElemWriter
        {
            friend constexpr auto adl_ListElem(ElemReader<Name, Index>)
            {
                return tag<Value>{};
            }
        };

        constexpr void adl_ListElem() {} // A dummy ADL target

        template <typename Name, std::size_t Index, typename Unique, typename = void>
        struct CalcSize : std::integral_constant<std::size_t, Index> {};

        template <typename Name, std::size_t Index, typename Unique>
        struct CalcSize < Name, Index, Unique, decltype(void(adl_ListElem(ElemReader<Name, Index>{}))) > : CalcSize<Name, Index + 1, Unique> {};

        template <typename Name, std::size_t Index, typename Unique>
        using ReadElem = typename decltype(adl_ListElem(ElemReader<Name, Index>{}))::type;

        template <typename Name, typename I, typename Unique>
        struct ReadElemList {};
        template <typename Name, std::size_t ...I, typename Unique>
        struct ReadElemList<Name, std::index_sequence<I...>, Unique> { using type = type_list<ReadElem<Name, I, Unique>...>; };
    }

    struct DefaultUnique {};

    template <typename T>
    struct DefaultPushBackUnique {};

    // Calculates the current list size
    template <typename Name, typename Unique = DefaultUnique>
    inline constexpr std::size_t size = impl::CalcSize<Name, 0, Unique>::value;

    // Touch this type to append `Value` to the list
    template <typename Name, typename Value, typename Unique = DefaultPushBackUnique<Value>>
    using PushBack = impl::ElemWriter<Name, size<Name, Unique>, Value>;

    // Returns the type previously passed to `WriteState`, or causes a SFINAE error
    template <typename Name, std::size_t I, typename Unique = DefaultUnique>
    using Elem = impl::ReadElem<Name, I, Unique>;

    // Returns the list elements as `Meta::TypeList<...>`
    template <typename Name, typename Unique = DefaultUnique>
    using Elems = typename impl::ReadElemList<Name, std::make_index_sequence<size<Name, Unique>>, Unique>::type;
}

// Each tag creates a different "counter" for types
struct CounterCommonTag {};

// Returns the index of `T`, or errors out if it wasn't registered
template <typename T>
constexpr std::size_t RegisteredTypeIndex = List::find_type<StatefulList::Elems<CounterCommonTag, T>, T>::value;

// Returns the total number of registered types.
// Don't mention this in the code until finish all registrations, otherwise the value will get stuck after the first mention.
// To work around this, you can pass different types to `Unique` to force a recalculation
template <typename Unique = StatefulList::DefaultUnique>
constexpr std::size_t RegisteredTypeCount = StatefulList::size<CounterCommonTag, Unique>;

// Registers a type
#define ENGINE_REGISTER_TYPE(type_) static_assert((void(StatefulList::PushBack<CounterCommonTag, type_>{}), true));

Solution

  • I don't entirely understand what your code attempts to do, but I think you essentially want a type registration using stateful template metaprogramming:

    #include <cstddef>
    #include <type_traits>
    #include <utility>
    
    template <typename T>
    struct tag {using type = T;};
    
    template <typename...>
    struct type_list {};
    
    namespace List
    {
        // Returns the index of an element in `type_list`, or causes an error if no such element.
        template <typename L, typename T>
        struct find_type {};
    
        template <typename T>
        struct find_type<type_list<>, T> {};
        template <typename F, typename ...P, typename T>
        struct find_type<type_list<F, P...>, T> : std::integral_constant<std::size_t, 1 + find_type<type_list<P...>,T>::value> {};
        template <typename F, typename ...P>
        struct find_type<type_list<F, P...>, F> : std::integral_constant<std::size_t, 0> {};
    }
    
    namespace StatefulList
    {
        namespace impl
        {
            template <typename Name, std::size_t Index>
            struct ElemReader
            {
                #if defined(__GNUC__) && !defined(__clang__)
                #pragma GCC diagnostic push
                #pragma GCC diagnostic ignored "-Wnon-template-friend"
                #endif
                friend constexpr auto adl_ListElem(ElemReader<Name, Index>);
                #if defined(__GNUC__) && !defined(__clang__)
                #pragma GCC diagnostic pop
                #endif
            };
    
            template <typename Name, std::size_t Index, typename Value>
            struct ElemWriter
            {
                friend constexpr auto adl_ListElem(ElemReader<Name, Index>)
                {
                    return tag<Value>{};
                }
            };
    
            constexpr void adl_ListElem() {} // A dummy ADL target.
    
            template <typename Name, std::size_t Index, typename Unique, typename = void>
            struct CalcSize : std::integral_constant<std::size_t, Index> {};
    
            template <typename Name, std::size_t Index, typename Unique>
            struct CalcSize<Name, Index, Unique, decltype(void(adl_ListElem(ElemReader<Name, Index>{})))> : CalcSize<Name, Index + 1, Unique> {};
    
            template <typename Name, std::size_t Index, typename Unique>
            using ReadElem = typename decltype(adl_ListElem(ElemReader<Name, Index>{}))::type;
    
            template <typename Name, typename I, typename Unique>
            struct ReadElemList {};
            template <typename Name, std::size_t ...I, typename Unique>
            struct ReadElemList<Name, std::index_sequence<I...>, Unique> {using type = type_list<ReadElem<Name, I, Unique>...>;};
        }
    
        struct DefaultUnique {};
    
        template <typename T>
        struct DefaultPushBackUnique {};
    
        // Calculates the current list size.
        template <typename Name, typename Unique = DefaultUnique>
        inline constexpr std::size_t size = impl::CalcSize<Name, 0, Unique>::value;
    
        // Touch this type to append `Value` to the list.
        template <typename Name, typename Value, typename Unique = DefaultPushBackUnique<Value>>
        using PushBack = impl::ElemWriter<Name, size<Name, Unique>, Value>;
    
        // Returns the type previously passed to `WriteState`, or causes a SFINAE error.
        template <typename Name, std::size_t I, typename Unique = DefaultUnique>
        using Elem = impl::ReadElem<Name, I, Unique>;
    
        // Returns the list elements as `Meta::TypeList<...>`.
        template <typename Name, typename Unique = DefaultUnique>
        using Elems = typename impl::ReadElemList<Name, std::make_index_sequence<size<Name, Unique>>, Unique>::type;
    }
    
    // Each such tag creates a different "counter" for types.
    struct MyCommonTag {};
    
    // Returns the index of `T`, or errors out if it wasn't registered.
    template <typename T>
    constexpr std::size_t MyTypeIndex = List::find_type<StatefulList::Elems<MyCommonTag, T>, T>::value;
    
    // Returns the total number of registered types.
    // Don't mention this in the code until finish all registrations, otherwise the value will get stuck after the first mention.
    // To work around this, you can pass different types to `Unique` to force a recalculation.
    template <typename Unique = StatefulList::DefaultUnique>
    constexpr std::size_t MyTypeCount = StatefulList::size<MyCommonTag, Unique>;
    
    // Registers a type.
    #define REGISTER_MY_TYPE(type_) static_assert((void(StatefulList::PushBack<MyCommonTag, type_>{}), true));
    
    REGISTER_MY_TYPE(int)
    static_assert(MyTypeIndex<int> == 0);
    
    REGISTER_MY_TYPE(float)
    static_assert(MyTypeIndex<float> == 1);
    
    REGISTER_MY_TYPE(char)
    static_assert(MyTypeIndex<char> == 2);
    
    static_assert(MyTypeIndex<int> == 0);
    static_assert(MyTypeIndex<float> == 1);
    static_assert(MyTypeIndex<char> == 2);
    static_assert(MyTypeCount<> == 3);
    

    Here types serve as your "enum constants".

    This doesn't work across multiple translation units, all registrations have to be visible when needed.