Search code examples
c++c++11templatestype-erasurecrtp

Type erasure: Retrieving value - type check at compile time


I have a limited set of very different types, from which I want to store instances in a single collection, specifically a map. To this end, I use the type erasure idiom, ie. I have a non-templated base class from which the templated, type specific class inherits:

struct concept
{
   virtual std::unique_ptr<concept> copy() = 0; // example member function
};

template <typename T>
struct model : concept
{
   T value;
   std::unique_ptr<concept> copy() override { ... }
}

I then store unique_ptrs to concept in my map. To retrieve the value, I have a templated function which does a dynamic cast to the specified type.

template <typename T>
void get(concept& c, T& out) {
   auto model = dynamic_cast<model<T>>(&c);
   if (model == nullptr) throw "error, wrong type";
   out = model->value;
}

What I don't like about this solution is, that specifying a wrong T is only detected at runtime. I'd really really like this to be done at compile time.

My options are as I see the following, but I don't think they can help here:

  • Using ad hoc polymorphism by specifying free functions with each type as an overload, or a template function, but I do not know where to store the result.

    • Using CRTP won't work, because then the base class would need to be templated.

    • Conceptually I would need a virtual function which takes an instance of a class where the result will be stored. However since my types are fundamentally different, this class would need to be templated, which does not work with virtual.

Anyways, I'm not even sure if this is logically possible, but I would be very glad if there was a way to do this.


Solution

  • For a limited set of types, your best option is variant. You can operate on a variant most easily by specifying what action you would take for every single variant, and then it can operate on a variant correctly. Something along these lines:

    std::unordered_map<std::string, std::variant<Foo, Bar>> m;
    
    m["a_foo"] = Foo{};
    m["a_bar"] = Bar{};
    
    for (auto& e : m) {
        std::visit(overloaded([] (Foo&) { std::cerr << "a foo\n"; }
                              [] (Bar&) { std::cerr << "a bar\n"; },
                   e.second);
    }
    

    std::variant is c++17 but is often available in the experimental namespace beforehand, you can also use the version from boost. See here for the definition of overloaded: http://en.cppreference.com/w/cpp/utility/variant/visit (just a small utility the standard library unfortunately doesn't provide).

    Of course, if you are expecting that a certain key maps to a particular type, and want to throw an error if it doesn't, well, there is no way to handle that at compile time still. But this does let you write visitors that do the thing you want for each type in the variant, similar to a virtual in a sense but without needing to actually have a common interface or base class.