Dynamically instantiate class templates

Viewed 135

there!

I am trying to create objects from a template class that as an enum as argument.

This is the code:

#include <iostream>
using std::cout;
using std::endl;

#include <string>
using std::string;

class Father
{
public:
    string name;
    Father(const string &n): name(n) { cout << "Father has been named " << name << endl; }
    virtual ~Father(void) { }
    virtual void whoami(void) = 0;
};

enum NATION { CANADA, USA, FRANCE, UK };

template<enum NATION nation>
class Son: public Father
{
public:
    string nationality;
    Son(const string &n): Father(n)
    {
         switch (nation)
         {
             case CANADA: nationality = "canadian"; break;
             case USA: nationality = "american"; break;
             case FRANCE: nationality = "french"; break;
             case UK: nationality = "british"; break;
         }
        cout << "I'm " << nationality << endl;
    }
    virtual ~Son(void) { }
    virtual void whoami(void);
};

template<enum NATION nation>
void Son<nation>::whoami(void) { cout << "My father is " << name << " and I'm " << nationality << endl; }
template<>
void Son<FRANCE>::whoami(void) { cout << "Mon père est " << name << " et je suis français" << endl; }

int main(int, const char *[])
{
    Father *canadian = new Son<CANADA>("James");
    enum NATION n = FRANCE;
    Father *french = new Son<n>("Pierre");
    
    canadian->whoami();
    french->whoami();
    
    delete canadian;
    delete french;
    
    return 0;
}

The code works well for the Canadian object, but not for the French one!

I get a compilation error on the Son<n>("Pierre"). It says that the template argument is not constant.

Code compiles fine if I replace "n" with "FRANCE" on the constructor invocation.

Is there anyway of doing this without the (silly!) switch option like:

switch(n)
{
  case CANADA: pointer = new Son<CANADA>("James"); break;
  case USA: pointer = new Son<USA>("James"); break;
  case FRANCE: pointer = new Son<FRANCE>("James"); break;
  case UK: pointer = new Son<UK>("James"); break;
}
3 Answers

As far as I know, there isn't! Actually, that's the whole point of templates being evaluated at compile-time (statically) and not at runtime (dynamically).
Here's a quick rep on how templates work in C++:
When you have a templated class

template <class T>
class MyClass {...};

this class won't actually be present in your compiled program by default. Only when some particular instantiation is actually needed, the compiler will generate this instantiation for you.
This website phrases it this way:

For example, to call GetMax [definition above] to compare two integer values of type int we can write:

int x,y;
GetMax<int>(x,y);

When the compiler encounters this call to a template function, it uses the template to automatically generate a function replacing each appearance of [generic type parameter] by the type passed as the actual template parameter (int in this case) and then calls it.

But if you dispatch to the type parameter at runtime, the compiler doesn't know which instance of the class to generate.

Another view to look at it is that each instantiation of your templated class must be individually represented in the bytecode of your program. But since the type parameter is not known at compile time, it is unable to generate the bytecode for a class which it doesn't know the generic types for.

Therefore, you have to reside to dispatching to the respective class instantiation at runtime, just like you suggested yourself. The other solution would be to bite the bullet and pass the parameter not as generic but as a normal one at runtime.

Sure, it's doable.

First, you need to know how many elements the enum has, so you need to add an extra "count" constant at the end:

enum NATION {CANADA, USA, FRANCE, UK, nation_count};

Next, you make an array (of size nation_count) of function pointers, with each function accepting const std::string &name, allocating the Son<...> object, and returning a Father pointer to it.

More or less like this:

Father *(*arr[nation_count])(const std::string &) = {
    [](const std::string &name) -> Father * {return new Son<CANADA>(name);},
    [](const std::string &name) -> Father * {return new Son<USA   >(name);},
    // And so on.
};

Then you can do arr[your_nation]("your_name") to call one of those functions, to construct the right object.

You can say that this doesn't look much better than manually writing a switch. Yes, but the good part is that you can generate the array of function pointers with templates:

template <typename T, T ...I, typename F>
constexpr auto make_array(std::integer_sequence<T, I...>, F &&func)
{
    return std::array{func(std::integral_constant<T, I>{})...};
}

std::unique_ptr<Father> make_son(NATION nation, const std::string &name)
{
    static constexpr auto funcs = make_array(
        std::make_integer_sequence<std::underlying_type_t<NATION>, nation_count>{},
        [](auto index)
        {
            constexpr NATION n = NATION(index.value);
            return +[](const std::string &name) -> std::unique_ptr<Father>
            {
                return std::make_unique<Son<n>>(name);
            };
        }
    );
    return funcs[nation](name);
}

Then use it like this:

NATION n = FRANCE;
auto french = make_son(n, "Pierre");   
french->whoami();

You can notice that I replaced raw pointers with std::unique_ptr. In real-life code you should try to never manage memory manually, and rely on containers and/or smart pointers.

Am I missing something but doesn't changing to

constexpr enum NATION n = FRANCE;

work for your particular program?

Related