MB.
MB.

Reputation: 1411

How to instantiate a list of types for compile-time/static polymorphism

I'm implementing a compile time dispatcher which makes use of static polymorphism and metaprogramming.

I have a list of types which I would like to instantiate into a runtime std::array.

struct Test
{
     typedef std::integral_constant<int,0> nop;
     typedef std::integral_constant<int,1> A;
     typedef std::integral_constant<int,2> B;
     typedef std::integral_constant<int,3> C;

     using list = mp_list<A, B, C>; // mp_list expands to: template<A, B, C> struct {};

     struct Things{
         int (Test::*process)(int foo, float bar);
         const std::string key;
         int something;
         float other;
     };

     typedef std::array<Things, mp_size<list>::value> Thing_list;
     Thing_list thing_list;

     template<typename T=nop> int process(int foo, float bar);

     // stuff...

     Test();
}

In the above code, mp_list is simply a variadic template which 'expands' to struct<A, B, C> mp_list. And likewise, mp_size gives an mp implementation of the sizeof.

As can be inferred, the Thing_list is an array with a compile-time known size.

I can then specialize a template function like so:

template<> int process<Test::B>(int foo, float bar){ /* do stuff */ };

to achieve compile-time polymorphism.

The above code works well, except that to initialize it, I am stuck at doing this in the constructor:

Test::Test() thing_list({{{&Test::process<A>, "A"}, // this all should be achieved through meta-programming
                          {&Test::process<B>, "B"},
                          {&Test::process<C>, "C"}}}} )
{
   // stuff
}

There are two things I'm unable to get right:

  1. I would like to have a MP based initialization of the list. As I update the list definition in the declaration, I would like my initialization to automatically reflect that list type.
  2. I would also like to avoid having to duplicate the name of the type as a string literal. I have tried to use something like integral_constant but the use of const char* as a template parameter seems to be forbidden. I am left to having to duplicate declaration (triplicate, really).

This answer is almost the solution: it takes a list of types and instantiates them like so:

static std::tuple<int*, float*, foo*, bar*> CreateList() {
return { Create<int>(), Create<float>(), Create<foo>(), Create<bar>() };
}

However, I'm stuck on converting from std::tuple to std::array.

The main question is #1. Bonus for #2 without using #define based trickery.

If anyone cares: this code is destined for embedded software. There are dozens of different types, and importantly, each type (e.g. A, B, C) will have an identically structured configuration to be loaded from memory (e.g. under a configuration key for "A") - hence the reason of wanting to have access to the string name of the type at runtime.

Upvotes: 1

Views: 688

Answers (2)

super
super

Reputation: 12928

I would suggest changing the typedefs for A, B and C to struct so you can define the string inside them.

struct A {
    static constexpr int value = 1;
    static constexpr char name[] = "A";
};

// Same for B and C

using list = mp_list<A, B, C>;

Then you can create a make_thing_list

template <typename... T>
static std::array<Things, sizeof...(T)> make_thing_list(mp_list<T...>) {
    return {{{&Test::process<T>, T::name}...}};
}

auto thing_list = make_thing_list(list{});

Complete example

#include <string>
#include <array>
#include <iostream>

template <typename... T>
struct mp_list {};

struct Test
{
    struct nop {
        static constexpr int value = 0;
        static constexpr char name[] = "nop";
    };
    struct A {
        static constexpr int value = 1;
        static constexpr char name[] = "A";
    };
    struct B {
        static constexpr int value = 2;
        static constexpr char name[] = "B";
    };
    struct C {
        static constexpr int value = 3;
        static constexpr char name[] = "C";
    };

     using list = mp_list<A, B, C>; // mp_list expands to: template<A, B, C> struct {};

     struct Things{
         int (Test::*process)(int foo, float bar);
         const std::string key;
         int something;
         float other;
     };

    template <typename... T>
    static std::array<Things, sizeof...(T)> make_thing_list(mp_list<T...>) {
        return {{{&Test::process<T>, T::name}...}};
    }

    using Thing_list = decltype(make_thing_list(list{}));

    Thing_list thing_list = make_thing_list(list{});

     template<typename T=nop> int process(int foo, float bar) {
         return T::value;
     }

     // stuff...

     Test() {}
};

int main() {
    Test t;

    static_assert(std::is_same_v<decltype(t.thing_list), std::array<Test::Things, 3>>);

    for (auto& thing : t.thing_list) {
        std::cout << thing.key << (t.*thing.process)(1, 1.0) << '\n';
    }
}

Upvotes: 1

max66
max66

Reputation: 66200

Not sure to understand what do you exactly want but...

Given that you can use at least C++17 (for auto template parameters), you can define outside your class some variables as

static constexpr char nops[] = "NOP";
static constexpr char A[] = "A";
static constexpr char B[] = "B";
static constexpr char C[] = "C";

Then a simple wrapper that accept nops, A, B, etc. as template parameters

template <auto val>
struct wrap
 { };

Then a using that, given a variadic list of template value parameters, create a mp_list of wrap types

template <auto ... vals>
using wrapper = mp_list<wrap<vals>...>;

At this point... I suppose that, inside Test, you can define nop and list as follows

using nop = wrap<nops>;

using list = wrapper<A, B, C>;

Using delegating constructor, meta-programming way to initialize your thing_list could be the following

template <auto ... vals>
Test (mp_list<wrap<vals>...>) 
   : thing_list{{{&Test::process<wrap<vals>>, vals}...}}
 { }

Test () : Test{list{}}
 { }

If you modify the list adding a D parameter (where D is the "D" literal)

using list = wrapper<A, B, C, D>;

automagically the you get an additional {&Test::process<wrap<D>>, D} element in your thing_list.

The following is a full compiling C++17 example

#include <array>
#include <string>
#include <type_traits>

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

template <typename>
struct mp_size;

template <typename ... Ts>
struct mp_size<mp_list<Ts...>>
   : public std::integral_constant<std::size_t, sizeof...(Ts)>
 { };

static constexpr char nops[] = "NOP";
static constexpr char A[] = "A";
static constexpr char B[] = "B";
static constexpr char C[] = "C";

template <auto val>
struct wrap
 { };

template <auto ... vals>
using wrapper = mp_list<wrap<vals>...>;


struct Test
 {
   using nop = wrap<nops>;

   using list = wrapper<A, B, C>;

   struct Things
    {
      int (Test::*process)(int foo, float bar);
      const std::string key;
      //   int something;
      //   float other;
    };

   using Thing_list = std::array<Things, mp_size<list>::value>;

   Thing_list thing_list;

   template<typename T=nop> int process(int foo, float bar)
    { return 0; }

   template <auto ... vals>
   Test (mp_list<wrap<vals>...>) 
      : thing_list{{{&Test::process<wrap<vals>>, vals}...}}
    { }

   Test () : Test{list{}}
    { }
 };

int main ()
 {
   Test  t;
 }

Upvotes: 2

Related Questions