这种创建异质函子容器的技术是否可以挽救?

Can this technique for creating a container of heterogenous functors be salvaged?

本文关键字:是否 技术 创建      更新时间:2023-10-16

这篇博文描述了一种创建异构指针容器的技术。基本的技巧是创建一个平凡的基类(即没有显式的函数声明,没有数据成员,什么都没有)和一个模板化的派生类,用于存储具有任意签名的std::function<>对象,然后使容器保存unique_ptr到基类的对象。代码也可以在GitHub上获得。

我不认为这段代码可以变得健壮;std::function<>可以从lambda创建,lambda可能包括捕获,捕获可能包括必须调用析构函数的重要对象的按值副本。当unique_ptr从映射中删除Func_t类型时,只调用它的(平凡的)析构函数,因此std::function<>对象永远不会被正确删除。

我已经用"非平凡类型"替换了GitHub上示例中的用例代码,然后通过lambda中的值捕获并添加到容器中。在下面的代码中,从示例中复制的部分在注释中注明;其他的都是我的。可能有一个更简单的问题演示,但我有点挣扎,甚至从这个东西得到一个有效的编译。

#include <map>
#include <memory>
#include <functional>
#include <typeindex>
#include <iostream>
// COPIED FROM https://plus.google.com/+WisolCh/posts/eDZMGb7PN6T
namespace {
  // The base type that is stored in the collection.
  struct Func_t {};
  // The map that stores the callbacks.
  using callbacks_t = std::map<std::type_index, std::unique_ptr<Func_t>>;
  callbacks_t callbacks;
  // The derived type that represents a callback.
  template<typename ...A>
    struct Cb_t : public Func_t {
      using cb = std::function<void(A...)>;
      cb callback;
      Cb_t(cb p_callback) : callback(p_callback) {}
    };
  // Wrapper function to call the callback stored at the given index with the
  // passed argument.
  template<typename ...A>
    void call(std::type_index index, A&& ... args)
    {
      using func_t = Cb_t<A...>;
      using cb_t = std::function<void(A...)>;
      const Func_t& base = *callbacks[index];
      const cb_t& fun = static_cast<const func_t&>(base).callback;
      fun(std::forward<A>(args)...);
    }
} // end anonymous namespace
// END COPIED CODE
class NontrivialType
{
  public:
    NontrivialType(void)
    {
      std::cout << "NontrivialType{void}" << std::endl;
    }
    NontrivialType(const NontrivialType&)
    {
      std::cout << "NontrivialType{const NontrivialType&}" << std::endl;
    }
    NontrivialType(NontrivialType&&)
    {
      std::cout << "NontrivialType{NontrivialType&&}" << std::endl;
    }

    ~NontrivialType(void)
    {
      std::cout << "Calling the destructor for a NontrivialType!" << std::endl;
    }
    void printSomething(void) const
    {
      std::cout << "Calling NontrivialType::printSomething()!" << std::endl;
    }
};
// COPIED WITH MODIFICATIONS
int main()
{
  // Define our functions.
  using func1 = Cb_t<>;
  NontrivialType nt;
  std::unique_ptr<func1> f1 = std::make_unique<func1>(
      [nt](void)
      {
        nt.printSomething();
      }
  );
  // Add to the map.
  std::type_index index1(typeid(f1));
  callbacks.insert(callbacks_t::value_type(index1, std::move(f1)));
  // Call the callbacks.
  call(index1);
  return 0;
}

这会产生以下输出(使用未优化的g++ 5.1):

NontrivialType{void}
NontrivialType{const NontrivialType&}
NontrivialType{NontrivialType&&}
NontrivialType{NontrivialType&&}
NontrivialType{const NontrivialType&}
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling NontrivialType::printSomething()!
Calling the destructor for a NontrivialType!

我数了5次构造函数调用和4次析构函数调用。我认为表明我的分析是正确的——容器不能正确地销毁它所拥有的实例。

这种方法是否可用?当我向Func_t添加虚拟=default析构函数时,我看到ctor/dtor调用的匹配数量:

NontrivialType{void}
NontrivialType{const NontrivialType&}
NontrivialType{NontrivialType&&}
NontrivialType{NontrivialType&&}
NontrivialType{const NontrivialType&}
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling NontrivialType::printSomething()!
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!

…所以我认为这个改变可能就足够了。是吗?

(注意:这种方法的正确性(或不正确性)与使用异构函数容器的想法是否好无关。在一些非常特殊的情况下,可能会有一些优点,例如,在设计解释器时;例如,Python类可以被看作是异构函数的容器加上异构数据类型的容器。但总的来说,我决定问这个问题是否不是表明我认为这在很多情况下可能是一个好主意

这基本上是有人试图实现类型擦除,并得到了可怕的错误。

是的,你需要一个虚析构函数。要删除的对象的动态类型显然是而不是Func_t,所以如果析构函数不是虚函数,它显然是UB。

无论如何,整个设计完全被破坏了。

类型擦除的重点是,你有一堆不同的类型共享一个共同的特征(例如:"可以用int调用并返回double"),并且您希望将它们变成具有该特征的单一类型(例如std::function<double(int)>)。从本质上讲,类型擦除是单向的:一旦您擦除了类型,如果不知道它是什么,就无法恢复它。

删除一个空类意味着什么?什么也没有,除了"这是一件事"。这是一个std::add_pointer_t<std::common_type_t<std::enable_if_t<true>, std::void_t<int>>>(更常见的是void*),在模板服装中被混淆了。

这个设计还有很多其他问题。因为类型被擦除为空,它必须恢复原始类型才能对它做任何有用的事情。但是,如果不知道它是什么,就无法恢复原始类型,因此它最终使用传递给Call的参数类型来推断存储在映射中的对象的类型。这是非常容易出错的,因为表示传递给Call的参数的类型和值类别的A...极不可能与std::function的模板参数的参数类型完全匹配。例如,如果你有一个std::function<void(int)>存储在那里,你试图用int x = 0; Call(/* ... */ , x);调用它,这是未定义的行为。图。

更糟糕的是,任何不匹配都隐藏在static_cast后面,导致未定义的行为,使其更难发现和修复。还有一个奇怪的设计,当你"知道"类型是什么时,要求用户传递一个type_index,但与这段代码的所有其他问题相比,这只是一个插曲。