Home > Software engineering >  Template type erasure
Template type erasure

Time:11-04

I am wondering whether there is a practical way of writing something like the following code using the C 17 standard:

#include <string>
#include <functional>
#include <unordered_map>

template <class Arg>
struct Foo
{
    using arg_type = Arg;
    using fun_type = std::function< void(Arg&) >;
    fun_type fun;
    
    void call( Arg& arg ) { fun(arg); }
};

struct Bar
{
    using map_type = std::unordered_map<std::string,Foo>; // that's incorrect
    map_type map;
    
    auto& operator[] ( std::string name ) { return map[name]; }
};

In the code above, the template argument of class Foo corresponds to the input type of some unary function which returns nothing. Different instances of Foo with different template types correspond to functions taking arguments of different types. The class Bar simply aims at assigning a name to these functions, but obviously the current declaration of the map is incorrect because it needs to know about the template type of Foo.

Or does it?

CodePudding user response:

Doing this with a compile-time check is, unfortunately, not feasible. You can, however, provide that functionality with a runtime check.

A map's value type can only be one single type, and Foo<T> is a different type for each T. However, we can work around this by giving every Foo<T> a common base class, have a map of pointers to it, and use a virtual function to dispatch call() to the appropriate subclass.

For this though, the type of the argument must also always be the same. As mentioned by @MSalters, std::any can help with that.

Finally, we can wrap all that using the pimpl pattern so that it looks like there's just a single neat Foo type:

#include <cassert>
#include <string>
#include <functional>
#include <any>
#include <unordered_map>
#include <memory>

struct Foo {
public:
  template<typename T, typename FunT>
  void set(FunT fun) {
      pimpl_ = std::make_unique<FooImpl<T, FunT>>(std::move(fun));
  }

  // Using operator()() instead of call() makes this a functor, which
  // is a little more flexible.
  void operator()(const std::any& arg) {
      assert(pimpl_);
      pimpl_->call(arg);
  }
  
private:
    struct IFooImpl {
      virtual ~IFooImpl() = default;
      virtual void call( const std::any& arg ) const = 0; 
    };

    template <class Arg, typename FunT>
    struct FooImpl : IFooImpl
    {
        FooImpl(FunT fun) : fun_(std::move(fun)) {}
        
        void call( const std::any& arg ) const override {
            fun_(std::any_cast<Arg>(arg));
        }

    private:
        FunT fun_;
    };

  std::unique_ptr<IFooImpl> pimpl_;
};


// Usage sample
#include <iostream>

void bar(int v) {
    std::cout << "bar called with: " << v << "\n";
}

int main() {
    std::unordered_map<std::string, Foo> table;

    table["aaa"].set<int>(bar);

    // Even works with templates/generic lambdas!
    table["bbb"].set<float>([](auto x) {
        std::cout << "bbb called with " << x << "\n";
    });

    table["aaa"](14);
    table["bbb"](12.0f);
}

see on godbolt

  • Related