0

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));
  • Unecessary use of macros (to save a bit of typing), and making your code less readable for everybody else. [Why are macros considered "evil"](https://stackoverflow.com/questions/14041453/why-are-preprocessor-macros-evil-and-what-are-the-alternatives) A bit of a dramatic post title but the answer is quite balanced. – Pepijn Kramer Jun 20 '23 at 17:07
  • So you are explaining HOW you are trying to do something, but WHAT do you want to achieve? Also note in current C++ you should use `enum class` [why is enum class preferred over plain enum](https://stackoverflow.com/questions/18335861/why-is-enum-class-preferred-over-plain-enum) – Pepijn Kramer Jun 20 '23 at 17:08
  • @PepijnKramer Doing this without macros would be great. I added a quick word at the start of what I want to achieve. – ingotangjingle Jun 20 '23 at 18:05
  • Still not clear enough for me. What do you mean an invalid enum values, all values in the enum should be valid and just accessed by their name (their actual value in most cases is irrelevant). And what do you mean by being unused, not being referred to anywhere in the code? I am not trying to be mean or anything but I just don't get it. – Pepijn Kramer Jun 20 '23 at 18:11
  • @PepijnKramer alright what about now? – ingotangjingle Jun 20 '23 at 18:46
  • I don't understand the idea either. You want each enum constant to exist only if the respective sources/feature is compiled in. But how could you possibly do this across multiple translation units? I see some ways to "assemble" such a enum (or something that looks like one) in a single translation unit from all included headers, but is this what you want? – HolyBlackCat Jun 20 '23 at 19:00
  • @HolyBlackCat Yes. Please post it as answer. – ingotangjingle Jun 20 '23 at 19:16

1 Answers1

0

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.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Can you add sources for further reading? Especially the ElemReader implementation would be great to understand! – ingotangjingle Jun 20 '23 at 20:08
  • @ingotangjingle "stateful template metaprogramming" is the magic word. All of it is mostly based on this same trick, so there should be a lot of explanations around. (In case it's not clear, all the lines starting with `#` in it are just to silence a GCC warning.) – HolyBlackCat Jun 20 '23 at 20:10
  • I'm going to reject the edit, because without the `static_assert`s the intended usage is not clear. – HolyBlackCat Jun 20 '23 at 20:15
  • static_asserts can't be used to answer the question though, as the individual values shouldn't be defined outside the REGISTER_MY_TYPE – ingotangjingle Jun 20 '23 at 20:40
  • @ingotangjingle Of course, I'm not suggesting to add the assets to your actual code. They're only here for demonstration purposes. You wouldn't be using `int`,`char`,etc as keys either, but rather some custom classes. – HolyBlackCat Jun 20 '23 at 20:49
  • This gets VERY close to the intended design, but sadly doesn't allow for compile-time flexibility: It requires `#include Foo` on every class using Foo as a feature, so if Foo isn't compiled, every file using Foo feature must also be changed to reflect this. – ingotangjingle Jun 22 '23 at 21:23
  • @ingotangjingle So use `#ifdef`s to disable features? You have the choice between that and a runtime feature registration, which which will delay getting the type indices to program startup. – HolyBlackCat Jun 22 '23 at 21:35
  • #ifdef isn't a reasonable solution, the non-compiled feature is still referenced in code. So it's fine if type indices are set at startup – ingotangjingle Jun 22 '23 at 21:43
  • @ingotangjingle Then like this: https://gcc.godbolt.org/z/934j3h9zK – HolyBlackCat Jun 22 '23 at 22:01
  • yep, that's it! Thanks! – ingotangjingle Jun 22 '23 at 22:23