【问题标题】:Template type erasure模板类型擦除
【发布时间】:2021-11-03 22:20:23
【问题描述】:

我想知道是否有使用 C++17 标准编写类似以下代码的实用方法:

#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]; }
};

在上面的代码中,Foo 类的模板参数对应于某个不返回任何内容的一元函数的输入类型。具有不同模板类型的Foo 的不同实例对应于采用不同类型参数的函数。 Bar 类只是旨在为这些函数分配一个名称,但显然当前映射的声明是不正确的,因为它需要知道 Foo 的模板类型。

是吗?

【问题讨论】:

  • 你想如何从地图中调用函数?如果传递了错误类型的 arg 会发生什么?
  • @HolyBlackCat 我预计会出现编译时错误,因为错误的类型被用作输入。我的预感是所有这些 Foo 实例应该具有基本相同的内存布局,并且编译器应该仍然知道参数类型是什么,因为它在类内部定义为arg_type。但当然,这可能只是一厢情愿(或者完全错误,我实际上不知道这些类是否具有相同的布局)。
  • 是什么阻止您将fun_type 设为std::any,而只将Foo::call&lt;Arg&gt; 设为模板?
  • @JonathanH 但是调用哪个函数取决于Arg,编译器应该如何知道在给定键处调用什么函数?
  • @Frank 这就是我问的原因;我不了解编译器的内部工作原理。我知道该类型实际上并未存储在内存中,但编译器通常可以很好地记住这些 typedef,而无需将它们存储在任何地方。

标签: c++ templates c++17 type-erasure


【解决方案1】:

不幸的是,通过编译时检查来执行此操作是不可行的。但是,您可以通过运行时检查来提供该功能。

一个映射的值类型只能是一个单一的类型,Foo&lt;T&gt; 是每个T 的不同类型。但是,我们可以通过为每个Foo&lt;T&gt; 提供一个公共基类、拥有指向它的指针映射并使用虚函数将call() 分派给适当的子类来解决这个问题。

为此,参数的类型也必须始终相同。正如@MSalters 所述,std::any 可以提供帮助。

最后,我们可以使用 pimpl 模式包装所有内容,这样看起来只有一个整洁的Foo 类型:

#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

【讨论】:

  • 太棒了!如果我们使用错误的输入类型调用函数,这似乎在运行时以bad any_cast 失败,这很好。我会用代码做更多实验,但我认为这就是我的答案!
  • 我很好奇你为什么用= default 定义IFooImpl 的析构函数:这只是一种好的做法,还是在这里真的很重要?我是否正确理解将unique_ptr 用于pimpl 模式也可以防止Foo 的实例被复制?
  • @JonathanH 因为我们要删除IFooImpl类型的指针,所以析构函数必须是virtual。我“可以”写了virtual ~IFooImpl() {},但使用default 会让我的意图更清晰。
  • @JonathanH 关于复制:正确。虽有可动。如果您希望它们可复制,您可以简单地使用std::shared_ptr,或者将clone() 虚拟方法添加到IFooImpl 并为Foo 编写一个复制/分配构造函数/运算符。
猜你喜欢
  • 2018-07-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多