这篇博文描述了一种创建异构指针容器的技术.基本技巧是创建一个简单的基类(即没有显式函数声明,没有数据成员,没有)和一个模板化的派生类,用于存储std::function<>具有任意签名的对象,然后使容器保持unique_ptrs到基类的对象.该代码也可以在GitHub上获得.
我不认为这个代码可以变得健壮; std::function<>可以从lambda创建,该lambda可能包含一个捕获,其中可能包含必须调用析构函数的非平凡对象的按值副本.当从地图中Func_t删除类型unique_ptr时,只会调用其(普通的)析构函数,因此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;
}
Run Code Online (Sandbox Code Playgroud)
这会产生以下输出(使用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!
Run Code Online (Sandbox Code Playgroud)
我计算五个构造函数调用和四个析构函数调用.我认为这表明我的分析是正确的 - 容器无法正确销毁它拥有的实例.
这种方法是否可以挽救?当我添加一个虚拟=default析构函数时Func_t,我看到匹配数量的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!
Run Code Online (Sandbox Code Playgroud)
......所以我认为这种改变可能就足够了.是吗?
(注意:这种方法的正确性 - 或缺乏 - 不依赖于异构函数容器的概念是否是一个好主意.在一些非常具体的情况下,可能有一些优点,例如,在设计时解释器;例如,Python类可能被认为是异构函数的容器加上异构数据类型的容器.但总的来说,我提出这个问题的决定并不表示我认为这可能是一个在很多情况下好主意.)
这基本上是有人试图实现类型擦除并使其严重错误.
是的,你需要一个虚拟析构函数.被删除的东西的动态类型显然不是Func_t,所以如果析构函数不是虚拟的,那么它显然是UB.
无论如何,整个设计完全被打破了.
类型擦除的关键在于你有一堆具有共同特征的不同类型(例如"可以用a 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来推断存储在地图中的东西的类型.这是非常容易出错的,因为A...,它代表传递给的参数的类型和值类别Call,极不可能完全匹配std::function模板参数的参数类型.例如,如果你有一个std::function<void(int)>存储在那里,并且你试图用a调用它int x = 0; Call(/* ... */ , x);,那么它是未定义的行为.去搞清楚.
更糟糕的是,任何不匹配都隐藏在static_cast导致未定义行为的背后,使得查找和修复变得更加困难.还有一个好奇的设计需要用户通过type_index,当你"知道"类型是什么时,但它与这个代码的所有其他问题相比只是一个副作用.