how can I achieve multiple conditional inheritance?

Viewed 2660

I have a type build that has a flag template, and according to the active flag bits, it inherits from those types. this allows me to "build" classes from many subclasses with a great number of configurations:

#include <type_traits>
#include <cstdint>

struct A { void a() {} };
struct B { void b() {} };
struct C { void c() {} };
struct D { void d() {} };

constexpr std::uint8_t FLAG_BIT_A = 0b1 << 0;
constexpr std::uint8_t FLAG_BIT_B = 0b1 << 1;
constexpr std::uint8_t FLAG_BIT_C = 0b1 << 2;
constexpr std::uint8_t FLAG_BIT_D = 0b1 << 3;

struct empty {};

template<std::uint8_t flags> 
using flag_a_type = std::conditional_t<(flags & FLAG_BIT_A), A, empty>;
template<std::uint8_t flags> 
using flag_b_type = std::conditional_t<(flags & FLAG_BIT_B), B, empty>;
template<std::uint8_t flags> 
using flag_c_type = std::conditional_t<(flags & FLAG_BIT_C), C, empty>;
template<std::uint8_t flags> 
using flag_d_type = std::conditional_t<(flags & FLAG_BIT_D), D, empty>;

template<std::uint8_t flags>
struct build : 
    flag_a_type<flags>, flag_b_type<flags>, flag_c_type<flags>, flag_d_type<flags> {

};

int main() {
    build<FLAG_BIT_A | FLAG_BIT_C> foo;
}

so build<FLAG_BIT_A | FLAG_BIT_C> should result in a class that inherits from A and from C.

but it doesn't compile, saying empty is already a direct base class:

error C2500: 'build<5>': 'empty' is already a direct base class

how can I achieve this without having to make 4 different empty structs to avoid the clash?

6 Answers

Here's another approach using that provides much more flexibility for bitmask-based inheritance:

#include <concepts>
#include <cstdint>

template <std::integral auto, class...>
struct inherit_mask {};

template <auto flags, class Base, class... Bases>
  requires((flags & 1) == 1)
struct inherit_mask<flags, Base, Bases...>
    : Base, inherit_mask<(flags >> 1), Bases...> {};

template <auto flags, class Base, class... Bases>
struct inherit_mask<flags, Base, Bases...>
    : inherit_mask<(flags >> 1), Bases...> {};

struct A { void a() {} };
struct B { void b() {} };
struct C { void c() {} };
struct D { void d() {} };

template <std::uint8_t flags>
using build = inherit_mask<flags, A, B, C, D>;

using foo = build<0b0101>;

static_assert(std::derived_from<foo, A>);
static_assert(not std::derived_from<foo, B>);
static_assert(std::derived_from<foo, C>);
static_assert(not std::derived_from<foo, D>);

Compiler Explorer

Works on clang, gcc, and msvc, and doesn't lead to exponential instantiation explosion.

Updated Answer

I'm editing this answer because it seems that only Clang compiles this code, even with the C++20 flag set. I believe Clang is correct. See the bottom of the answer for a quick investigation into it with an MRE.

So, this revised answer no longer has a requirement for C++20, in exchange for just a bit more non-generic typing:

Change your empty class to:

template<typename T>
class empty { };

Then you can modify each type alias to use empty<X>.

template<std::uint8_t flags> using flag_a_type = std::conditional_t<(flags & FLAG_BIT_A) != 0, A, empty<A>>;
template<std::uint8_t flags> using flag_b_type = std::conditional_t<(flags & FLAG_BIT_B) != 0, B, empty<B>>;
template<std::uint8_t flags> using flag_c_type = std::conditional_t<(flags & FLAG_BIT_C) != 0, C, empty<C>>;
template<std::uint8_t flags> using flag_d_type = std::conditional_t<(flags & FLAG_BIT_D) != 0, D, empty<D>>;

Old Answer Below

In C++20, a common trick to generate unique types in a template instantiation is to use an empty lambda: []{}

You can then create each of your flag_x_type type aliases as so:

template<std::uint8_t flags> using flag_a_type = std::conditional_t<(flags& FLAG_BIT_A) == 1, A, decltype([]{})>;

This even maintains standard layout, which is nice.

Appendix

Clang successfully compiles my old answer, MSVC and GCC fail both fail. I believe Clang is correct here since it also works on all the cases "leading up" where both fail without changing any significant meaning about the code.

MSVC breaks on even attempting to use the type alias as an inheritance target (but only if the alternative is an empty class instead of an unevaluated lambda) GCC breaks when attempting to use the member function provided by the base class, even though it can use it when the alias is "inlined".

Minimum reproducible example of the failure: https://godbolt.org/z/7MEnn3bsf

in the meantime, I thought about a solution myself. Ideally I don't want to bloat the scope with empty types or introduce many temporary types or inheritance chains. So my idea is to create an std::tuple from the choices of A, B, C and D. This assumes that each bit is set to a type, which is the case for my application.

template<std::uint8_t flags, typename... types>
constexpr auto flags_to_tuple() {
    using flag_tuple = std::tuple<types...>;

    constexpr auto SET_BITS = std::popcount(flags); //counts the number of set bits

    constexpr auto flags_to_array = [&]() {
        std::array<unsigned, SET_BITS> result{};
        unsigned ctr = 0u;
        for (unsigned i = 0u; i < sizeof(flags) * 8; ++i) { //check each bit in flags
            if (flags & (1 << i)) {
                result[ctr] = i;
                ++ctr;
            }
        }
        return result;
    };
    constexpr auto bit_array = flags_to_array();

    auto make_tuple = [&]<typename I, I... indices>(std::index_sequence<indices...>) {
        return std::tuple<std::tuple_element_t<bit_array[indices], flag_tuple>...>{};
    };
    return make_tuple(std::make_index_sequence<SET_BITS>());
}

so

decltype(flags_to_tuple<FLAG_BIT_A | FLAG_BIT_C, A, B, C, D>());

will result in std::tuple<A, C>. With this I can implement build class inheriting only from the std::tuple types:

template<class A, template<class...> class B> struct rename_impl {};

template<
    template<class...> class A, class... T,
    template<class...> class B> struct rename_impl<A<T...>, B> {
    using type = B<T...>;
};

template<typename... types>
struct build_impl : types... {

};

template<std::uint8_t flags>
using flags_tuple_type = decltype(flags_to_tuple<flags, A, B, C, D>());

template<std::uint8_t flags>
using build = typename rename_impl<flags_tuple_type<flags>, build_impl>::type;

int main() {
    build<FLAG_BIT_A | FLAG_BIT_C> foo;
    foo.a();
    foo.c();
}

I no longer need any std::conditional or empty types with this flag to tuple to variadic conversion. see the full code on godbolt

Not inheriting from the same empty struct multiple times is good, because empty base optimization can't make several structs of the same type overlap, potentially making your class larger than it should be.

You don't need no fancy templates to do it:

constexpr std::uint8_t FLAG_BIT_A = 0b1 << 0;
constexpr std::uint8_t FLAG_BIT_B = 0b1 << 1;
constexpr std::uint8_t FLAG_BIT_C = 0b1 << 2;

template <bool> struct A { void a() {} };
template <> struct A<false> {};
template <bool> struct B { void b() {} };
template <> struct B<false> {};
template <bool> struct C { void c() {} };
template <> struct C<false> {};

template<std::uint8_t flags>
struct build : A<flags & FLAG_BIT_A>, B<flags & FLAG_BIT_B>, C<flags & FLAG_BIT_C>
{};

This is a bit of a hacky solution as well (it's a bit harder to add new flags, the flag_x_type classes are not independent of each other and it's a bit more verbose), but your build would only inherit empty once, and you would not need four individual empty types:

#include <type_traits>
#include <cstdint>

struct A {  void a() {} };
struct B {  void b() {} };
struct C {  void c() {} };
struct D {  void d() {} };

constexpr std::uint8_t FLAG_BIT_A = 0b1 << 0;
constexpr std::uint8_t FLAG_BIT_B = 0b1 << 1;
constexpr std::uint8_t FLAG_BIT_C = 0b1 << 2;
constexpr std::uint8_t FLAG_BIT_D = 0b1 << 3;

struct empty {};

template<std::uint8_t flags> using flag_a_type = std::conditional_t<(flags& FLAG_BIT_A), A, empty>;

template<std::uint8_t flags> struct _flag_b_type : B, flag_a_type<flags> {};
template<std::uint8_t flags> struct flag_b_type : std::conditional_t<(flags& FLAG_BIT_B), _flag_b_type<flags>, flag_a_type<flags>>  {};

template<std::uint8_t flags> struct _flag_c_type : C, flag_b_type<flags> {};
template<std::uint8_t flags> struct flag_c_type : std::conditional_t<(flags& FLAG_BIT_C), _flag_c_type<flags>, flag_b_type<flags>> {};

template<std::uint8_t flags> struct _flag_d_type : D, flag_c_type<flags> {};
template<std::uint8_t flags> struct flag_d_type : std::conditional_t<(flags& FLAG_BIT_D), _flag_d_type<flags>, flag_c_type<flags>> {};

template<std::uint8_t flags>
struct build : flag_d_type<flags> {

};

int main() {
    build<FLAG_BIT_A | FLAG_BIT_C> foo;
    foo.a();
    //foo.b(); // error: 'struct build<5>' has no member named 'b'
    foo.c();
    //foo.d(); // error: 'struct build<5>' has no member named 'd'
}

Here's one way to do inheritance without repetition (I'm sure it can be simplified a lot)

#include <type_traits>

// --- Check if the first argument is the same as one of the rest -- //

template <typename First, typename ... Rest>
constexpr bool is_one_of = (std::is_same_v<First, Rest> || ...);

// --- Generic type list -- //

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

// --- Build type list from head and tail -- //

template <typename, typename> 
struct cons_helper; 

template <typename head, typename... tail>
struct cons_helper <head, type_list<tail...>> {
    using type = type_list<head, tail...>;
};

template <typename car, typename cdr> 
using cons = typename cons_helper<car, cdr>::type;

// -- Build a list with no repeated elements out of given arguments -- //

template <typename...>
struct unique_list_helper;

template <>
struct unique_list_helper <> {
    using type = type_list<>;
};

template <typename first, typename ... rest>
struct unique_list_helper <first, rest...> {
    using type = std::conditional_t<
                          is_one_of<first, rest...>,
                          typename unique_list_helper<rest...>::type,
                          cons<first, typename unique_list_helper<rest...>::type>
                          >;
};

template <typename... args>
using unique_list = typename unique_list_helper<args...>::type;

// -- Build a class that inherits from all its arguments, ignoring duplicates -- //

template <typename> struct inherit_from_once_helper;

template <typename ... args>
struct inherit_from_once_helper<type_list<args...>> : public args... {};

template <typename ... args>
using inherit_from_once = inherit_from_once_helper<unique_list<args...>>;

Live Demo

One downside is that both branches in std::conditional_t are instantiated, which may or may not lead to exponential instantiation explosion. It can be fixed but I don't know of any pretty solution. Share if you do.

Related