C++11 的 Any 类型擦除实现是否安全,会触发 UB 吗?
对于示例中的 int、std::string 以及按具体类型创建的普通类对象,get() 的指针恢复和删除器的处理符合 C++11 规则,多态和多重继承本身不会导致 UB。原实现没有限制可存储的类型,因此不能认定所有模板实参都安全:数组类型会破坏当前的分配与释放配对,需要明确禁止或单独实现支持。
为什么 get() 的转换成立
以存储 int 为例,指针经历的转换是:
new int(...) → int* → void* → int*
对象指针转换为 void* 后,再转换回原对象类型的指针,可以恢复原来的指针值;恢复时也允许增加 cv 限定。这是语言规则提供的保证,与对象是否为 standard-layout 无关。参见 N3337 的 static_cast 规则。
原实现通过以下条件限制恢复类型:
if (m_type == typeid(T)) {
return static_cast<T *>(m_value.get());
}
当存储类型限定为无顶层 cv 限定的非数组对象类型时,这个判断足以支持当前的取值方式。类型不匹配时会返回 nullptr,不会进入转换分支。
typeid 会忽略顶层 const、volatile 和引用形式。例如,typeid(int) == typeid(const int) 为真。因此,这里的比较并非保留所有限定符的严格类型身份比较。参见 N3337 的 typeid 规则。
对于实际存储的非 const int,调用 get<const int>() 合法,只是取得一个只读指针。原实现的 make<const int>() 则会编译失败,因为 const int* 不能隐式转换为成员所需的 void*。这是接口限制,不是运行时 UB。
多重继承中的地址差异不会影响当前实现
当前实现没有先将派生类指针转换成基类指针。存储 Derived 时,路径始终是:
Derived* → void* → Derived*
get<Base>() 不会匹配 typeid(Derived),因此返回 nullptr。基类子对象是否位于不同地址,不影响这一流程。
如果需要取得基类指针,应先恢复具体类型,再进行正常的派生类到基类转换:
struct Left {
int left = 1;
};
struct Right {
int right = 2;
};
struct Derived : Left, Right {};
Any value = Any::make<Derived>();
Derived* derived = value.get<Derived>();
Right* right = derived; // 编译器负责必要的地址调整
Right* direct = value.get<Right>(); // nullptr
不能只放宽类型检查,就直接把保存的 void* 转成某个基类指针。这样不会执行从 Derived* 到基类指针所需的转换。
自定义删除器是否正确
对于通过 new T(...) 创建的非数组对象,下面的删除器可以恢复具体类型并销毁对象:
[](const void *ptr) {
const T *self = static_cast<const T *>(ptr);
delete self;
}
通过 const T* 删除对象是合法的,const 不会阻止析构。存储 Derived 时,删除器也按 Derived 实例化,因此不会因为某个基类没有虚析构函数而产生“通过基类指针删除派生类对象”的问题。相关规则见 N3337 的 delete 说明。
删除器 lambda 没有捕获,可以转换为对应的函数指针,因此能用作这里的 std::unique_ptr 删除器。
数组是需要修正的边界
考虑下面的模板实参:
auto values = Any::make<int[3]>();
当 T 是 int[3] 时,new T() 创建数组,返回首元素指针;删除器却使用普通的 delete self。标准要求数组分配与数组释放方式配对,原实现不能为这种用法提供安全保证。问题来自模板实例化后的实际分配类型,即使源码写的是 new T(),也不能忽略数组情形。参见 N3337 的数组释放要求。
对这个轻量实现来说,直接禁止原生数组更容易保持正确。需要保存固定长度数组时,可以存储 std::array<int, 3>;它是普通类对象,适用当前的单对象分配和删除方式。
如何收紧接口
先显式包含用到的头文件,尤其是声明 std::forward 的 <utility>,不要依赖 <memory> 间接包含它:
#include <typeinfo>
#include <memory>
#include <utility>
#include <type_traits>
然后将原来的 make() 替换为:
template <typename T, typename... Args>
static Any make(Args &&... args) {
static_assert(
std::is_object<T>::value && !std::is_array<T>::value,
"T must be a non-array object type"
);
static_assert(
!std::is_const<T>::value && !std::is_volatile<T>::value,
"T must not have top-level cv qualifiers"
);
return Any(TypeSelector<T>{}, std::forward<Args>(args)...);
}
这会明确排除数组、引用、函数、void 和带顶层 cv 限定的存储类型。T 还需要在实例化处完整,并且能够用传入参数构造、能够被删除;现有的 new T(...) 和删除器会进一步约束这些要求。
使用时还要注意什么
get<T>()返回借用指针。不要自行delete,也不要在所指对象销毁后解引用。- 默认移动构造会转移
m_value的所有权。移出对象保留原来的类型信息,但其指针为空,因此调用合法类型的get<T>()会返回nullptr。 - 原实现没有提供复制操作,也没有可用的移动赋值操作。
m_type是引用成员,不能直接依靠默认移动赋值来更新类型信息。 - 若需要从
const Any读取,应另外提供返回const T*的 const 成员函数重载。
保留现有的类型擦除思路即可:补齐头文件、限制存储类型,并让调用方遵守返回指针的生命周期约束。不必仅因涉及继承就把 static_cast 改成其他转换。
备注:内容仅供参考。
