这个基于C++11的类型擦除实现是安全的吗,还是会导致未定义行为?
我想出了一个类似 std::any 的东西的C++11实现。根据我的测试,它工作正常,但我担心自己在某处可能触发了未定义行为,尚未察觉。
我特别担心 Any::get() 的实现,以及我这个 Any 类与多态类型之间的交互(子对象的地址不一定等同于父对象的地址)。
我的实现安全吗?
我的实现:
#include <typeinfo>
#include <memory>
class Any {
private:
template <typename T>
class TypeSelector {};
const std::type_info &m_type;
std::unique_ptr<void, void(*)(const void *)> m_value;
template <typename T, typename... Args>
Any(TypeSelector<T>, Args &&... args) :
m_type(
typeid(T)
),
m_value(
std::unique_ptr<void, void(*)(const void *)> {
new T (
std::forward<Args>(args)...
),
[](const void *ptr) {
const T *self = static_cast<const T *>(ptr);
delete self;
}
}
)
{}
public:
Any(Any &&) = default;
template <typename T, typename... Args>
static Any make(Args &&... args) {
return Any(TypeSelector<T> {}, std::forward<Args>(args)...);
}
template <typename T>
T *get() {
if (m_type == typeid(T)) {
return static_cast<T *>(m_value.get());
} else {
return nullptr;
}
}
};
示例用法:
#include <string>
#include <iostream>
template <typename T>
void print(const T *value) {
if (value) {
std::cout << *value << '\n';
} else {
std::cout << "nullptr" << '\n';
}
}
int main() {
Any str = Any::make<std::string>("Hello there");
Any num = Any::make<int>(54);
print(str.get<std::string>()); // Hello there
print(str.get<int>()); // nullptr
print(num.get<std::string>()); // nullptr
print(num.get<int>()); // 54
return 0;
}
解决方案
我会把指向typeinfo的指针替代引用。这会让移动操作更稳妥。(不过请把空指针也视为空指针处理。)
输入和输出的类型几乎要完全匹配。
不会产生未定义行为。
在动态链接环境中,某些在创建它们的DLL卸载后才失效的any可能会崩溃。我用ADL技巧来确保any在类型库中被创建来解决这个问题。
当你用模板来创建一个函数指针时,该指针是在模板被实例化的那个动态库中创建的。如果在调用该指针之前该动态库就被卸载,程序就会崩溃,因为相应的代码不再存在。
template<class T>
struct tag_t {};
struct bob {
template<class T, class...Args>
bob( tag_t<T>, Args&&...args ):
m_pData( new T{std::forward<Args>(args)...} ),
m_pDestroy( +[](void* ptr){ delete static_cast<T*>(ptr); } )
{}
~bob() { if (m_pDestroy) m_pDestroy(m_pData); }
bob(bob&& o) {
using std::swap;
swap(m_pData, o.m_pData);
swap(m_pDestroy, o.m_pDestroy);
}
bob& operator=(bob&& o) {
using std::swap;
swap(m_pData, o.m_pData);
swap(m_pDestroy, o.m_pDestroy);
return *this;
}
private:
void* m_pData = nullptr;
void(*m_pDestroy)(void*) = nullptr;
};
这是一个非常简单的any,名为 bob,它只支持销毁所存储的内容并将数据移动到另一个 bob 中。(你甚至无法访问对象本身。)
问题在于m_pDestroy究竟存放在哪里。
设想我们有一个 chicken.dll、一个 coop.dll 和一个 delivery.dll。这个coop存储着一堆 bob 对象,但并不知道 delivery.dll。
我们把鸡放在 bobs中,存在一个 coop.dll 的全局结构里。这些 bobs是在 farm.dll 创建的;因此 m_pDestroy 代码位于 delivery.dll。
如果在 coop.dll 被卸载之前,任一 delivery.dll 或 chicken.dll 被卸载,当我们销毁那些 bob 对象时,m_pDestroy 将访问已不再存在于内存中的代码。就会崩溃。
你可以使用一些技巧来确保 m_pDestroy 存在于某个特定的DLL中,例如:
using Destroyer = void(*)(void*);
namespace Birds {
Destroyer get_dll_safe_destroyer( tag_t<Chicken> );
}
把 get_dll_safe_destroyer 放在与 Chicken 相同的命名空间里。
然后在bob我们执行:
struct bob {
template<class T, class...Args>
bob( tag_t<T>, Args&&...args ):
m_pData( new T{std::forward<Args>(args)...} ),
m_pDestroy( get_dll_safe_destroyer(tag_t<T>{}) )
{}
~bob() { if (m_pDestroy) m_pDestroy(m_pData); }
bob(bob&& o) {
using std::swap;
swap(m_pData, o.m_pData);
swap(m_pDestroy, o.m_pDestroy);
}
bob& operator=(bob&& o) {
using std::swap;
swap(m_pData, o.m_pData);
swap(m_pDestroy, o.m_pDestroy);
return *this;
}
private:
void* m_pData = nullptr;
void(*m_pDestroy)(void*) = nullptr;
};
再加上一个
namespace bob_helpers{
template<class T>
Destroyer implement_dll_safe_destroyer( tag_t<T> ) {
return +[](void* ptr){ delete static_cast<T*>(ptr); };
}
}
并在 chicken.dll 内部导出
Destroyer Birds::get_dll_safe_destroyer( tag_t<Chicken> tag ) {
return implement_dll_safe_destroyer(tag);
}
现在每个带有 Chicken 的 bob 都会从 chicken.dll 获得 m_pDestroy 函数,而不是来自于创建它的那个包含 bob 的 Chicken 所在的dll。
这并不总是成为问题,因为它要求你的类型擦除对象在DLL卸载期间仍然存在,并且那些在 chicken.dll 将 Chicken 放入 bob 之前就被卸载的库也会遇到问题。因此,并不总是会触发问题。