Marius Herzog
Marius Herzog

Reputation: 597

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:

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.

Upvotes: 4

Views: 1960

Answers (3)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275270

Your runtime check occurs at the point where you exit type erasure.

If you want to compile time check the operation, move it within the type erased boundaries, or export enough information to type erase later.

So enumerate the types, like std variant. Or enumerate the algorithms, like you did copy. You can even mix it, like a variant of various type erased sub-algorithms for the various kinds of type stored.

This does not support any algorithm on any type polymorphism; one of the two must be enumerated for things to resolve at compile time and not have a runtime check.

Upvotes: 1

Nir Friedman
Nir Friedman

Reputation: 17704

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.

Upvotes: 2

user4442671
user4442671

Reputation:

You cannot do compile-time type checking for an erased type. That goes against the whole point of type erasure in the first place.

However, you can get an equivalent level of safety by providing an invariant guarantee that the erased type will match the expected type.

Obviously, wether that's feasible or not depends on your design at a higher level.

Here's an example:

class concept {
public:
  virtual ~concept() {}
};

template<typename T>
struct model : public concept { 
  T value;
};

class Holder {
public:
  template<typename T>
  void addModel() {
    map.emplace(std::type_index(typeid(T)), std::make_unique<model<T><());
  }

  template<typename T>
  T getValue() {
    auto found = types.find(std::type_index(typeid(T)));
    if(found == types.end()) {
      throw std::runtime_error("type not found");
    }

    // no need to dynamic cast here. The invariant is covering us.
    return static_cast<model<T>*>(found->second.get())->value;
  }

private:
  // invariant: map[type] is always a model<type>
  std::map<std::type_index, std::unique_ptr<concept>> types;
};

The strong encapsulation here provides a level of safety almost equivalent to a compile-time check, since map insertions are aggressively protected to maintain the invariant.

Again, this might not work with your design, but it's a way of handling that situation.

Upvotes: 1

Related Questions