Search code examples
c++c++11variadic-templates

C++11 indexing template parameter packs at runtime in order to access Nth type


From this SO topic (and this blog post), I know how to access Nth type in a template parameter pack. For instance, one of the answers to the abovementioned SO question suggests this:

template<int N, typename... Ts> using NthTypeOf = typename std::tuple_element<N, std::tuple<Ts...>>::type;

using ThirdType = NthTypeOf<2, Ts...>;

However, these methods work only in compile-time. Trying to do something such as:

int argumentNumber = 2;
using ItsType = NthTypeOf<argumentNumber, Arguments...>;

would result in compile error:

Error : non-type template argument is not a constant expression

Is there a way to access Nth type at runtime?


Here's my use case:

My program reads a text file, which is basically an array of numbers. Each number i refers to the i-th type of a template parameter pack that my class is templated based on. Based on that type, I want to declare a variable of that type and do something differently with it. For example, if it's a string, I want to declare a string and do string matching, and if it's an integer, I would like to compute the square root of a number.


Solution

  • C++ is a statically​ typed language. As such the type of all variables needs to be known at compile time (and cannot vary). You want a type that depends on a runtime value. Luckily C++ also features dynamic typing of objects.

    Warning: all code in this answer serves only for demonstration of the basic concept/idea. It's missing any kind of error handling, sane interfaces (constructors...), exception safety, ... . So don't use for production, consider using the implementations​ available from boost.

    To use this feature you need what's called a polymorphic base class: a class with (at least) one virtual member function from which you derive further classes.

    struct value_base {
      // you want to be able to make copies
      virtual std::unique_ptr<value_base> copy_me() const = 0;
      virtual ~value_base () {}
    };
    
    template<typename Value_Type>
    struct value_of : value_base {
      Value_Type value;
    
      std::unique_ptr<value_base> copy_me() const {
        return new value_of {value};
      }
    };
    

    You can then have a variable with static type of pointer or reference to that base class, which can point to/reference objects from both the base class as well as from any of those derived classes. If you have a clearly defined interface, then encode that as virtual member functions (think of Shape and area (), name (), ... functions) and make calls through that base class pointer/reference (as shown in the other answer). Otherwise use a (hidden) dynamic cast to obtain a pointer/reference with static type of the dynamic type:

    struct any {
      std:: unique_ptr<value_base> value_container;
    
      // Add constructor
    
      any(any const & a)
        : value_container (a.value_container->copy_me ())
      {}
      // Move constructor
    
      template<typename T>
      T & get() {
        value_of<T> * typed_container
            = dynamic_cast<value_of<T> *>(value_container.get();)
        if (typed_container == nullptr) {
          // Stores another type, handle failure
        }
        return typed_container->value;
      }
    
      // T const & get() const;
      // with same content as above
    };
    
    template<typename T, typename... Args>
    any make_any (Args... && args) {
      // Raw new, not good, add proper exception handling like make_unique (C++14?)
      return {new T(std:: forward<Args>(args)...)};
    }
    

    Since object construction is done at runtime the actual type of the pointed to/referenced object may depend on runtime values:

    template<typename T>
    any read_and_construct (std:: istream & in) {
      T value;
      // Add error handling please
      in >> value;
      return make_any<T>(std:: move (value));
    }
    
    // ...
    
    // missing: way of error handling
    std::map<int, std:: function<any(std:: istream &)>> construction_map;
    construction_map.insert(std::make_pair(1, read_and_construct<double>));
    // and more
    int integer_encoded_type;
    // error handling please
    cin >> integer_encoded_type;
    // error handling please
    any value = construction_map [integer_encoded_type] (cin);
    

    As you may have noticed above code uses also a clearly defined interface for construction. If you don't intend to do lots of different things with the returned any objects, potentially storing them in various data structures over great parts of the time your program is running, then using an any type is most likely overkill and you should just put the type dependent code into those construction functions, too.

    A serious drawback of such an any class is its generality: it's possible to store just about any type within it. This means that the (maximum) size of the (actually) stored object is not known during compilation, making use of storage with automatic duration (the "stack") impossible (in standard C++). This may lead to expensive usage of dynamic memory (the "heap"), which is considerably slower than automatic memory. This issue will surface whenever many copies of any objects have to be made, but is probably irrelevant (except for cache locality) if you just keep a collection of them around.

    Thus, if you know at compile time the set of types which you must be able to store, then you can (at compile time) compute the maximum size needed, use a static array of that size and construct your objects inside that array (since C++11 you can achieve the same with a (recursive template) union, too):

    constexpr size_t max_two (size_t a, size_t b) {
      return (a > b) ? a : b;
    }
    
    template<size_t size, size_t... sizes>
    constexpr size_t max_of() {
      return max_two (size, max_of<sizes>());
    }
    
    template<typename... Types>
    struct variant {
      alignas(value_of<Types>...) char buffer[max_of<sizeof (value_of<Types>)...>()];
      value_base * active;
    
      // Construct an empty variant
      variant () : active (nullptr)
      {}
    
      // Copy and move constructor still missing!
    
      ~variant() {
        if (active) {
          active->~value_base ();
        }
      }
    
      template<typename T, typename... Args>
      void emplace (Args... && args) {
        if (active) {
          active->~value_base ();
        }
        active = new (buffer) T(std:: forward<Args>(args)...);
      }
    };