从 unique_ptr<T> 的 void* 转换为 T** 是如何工作的?
Posted
技术标签:
【中文标题】从 unique_ptr<T> 的 void* 转换为 T** 是如何工作的?【英文标题】:How does casting from a void* of unique_ptr<T> to T** work? 【发布时间】:2017-09-13 17:59:58 【问题描述】:我对我在 VC++ 2015 中看到的结果感到惊讶,需要帮助来了解它的工作原理。
struct MyType
MyType(int x_) : x(x_)
int x;
;
auto u = std::make_unique<MyType>(10);
void* pv = &u;
这显然失败了,因为u
的地址不是指向MyType
的指针:
MyType *pM = (MyType*)pv;
但这行得通,pM2
获取存储在u
中的MyType
对象的地址:
MyType** ppM = (MyType**)pv;
MyType* pM2 = *ppM;
标准中是否有任何内容表明这应该有效?还是仅由于我的编译器的不可移植实现细节而起作用?有什么东西可以让我将unique_ptr
视为一个循环的指针?
在您说“这很愚蠢,不要使用 void*
或 C 样式转换”之前,请理解我正在使用通过 void 指针和结构成员偏移处理结构序列化的遗留代码。我现在不能改变那部分。但我想为结构成员使用unique_ptr
来简化内存所有权和清理。而且我想知道我的unique_ptr
在这个遗留环境中是多么脆弱。
【问题讨论】:
你为什么不直接做MyType* pv = u.get();
? en.cppreference.com/w/cpp/memory/unique_ptr/get
以上各项的有效/无效是什么意思?在每种情况下,您是否都试图获取指向 unique_ptr
拥有的对象的指针?如果是,MyType**
演员表是什么?只需使用 void *pv = u.get();
unique_ptr
,它有一个无状态删除器,你的删除器通常只包含一个指向托管对象的指针,这就是你的类型双关语似乎起作用的原因。
你很幸运,因为std::unique_ptr
很可能是一个具有单个指针属性的结构,并且碰巧std::unique_ptr
类型的对象的地址与第一个属性的地址相同这个对象,也就是指针。
抛弃不安全的 C 风格转换。如果您必须强制转换(总是尝试不),请使用C++强制转换。但甚至不清楚你真正想要的是演员表。
我正在编写的代码没有进行这种转换/类型双关语。它是执行 C 风格转换的通用序列化代码。它不是为智能指针制作的,它是在不久的将来不会改变的遗留代码。我刚刚尝试在通过序列化的结构中使用 unique_ptr 。从目前的答案来看,听起来我应该坚持使用原始指针和析构函数。
【参考方案1】:
这基本上只是你运气好。
在特定编译器的 ABI 中,存储由 unique_ptr
维护的对象的 T*
是对象的第一个成员,因此它与对象本身具有相同的地址。与此示例大致相同:
struct container
int val;
;
int main()
container c15;
intptr_t val1 = reinterpret_cast<intptr_t>(&c);
intptr_t val2 = reinterpret_cast<intptr_t>(&(c.val));
assert(val1 == val2); //will pretty much always be true
当然,这不是你应该依赖的行为!标准未指定它,如果供应商决定他们有更好的格式来存储 std::unique_ptr
中的指针,则可能会发生变化。 p>
【讨论】:
【参考方案2】:基本上你正在做这样的事情:
std::unique_ptr<MyType> up = ...;
MyType* p = *reinterpret_cast<MyType**>(&up);
有一些弯路和 C 风格的演员表。您将指向unique_ptr
的指针重新解释为指向MyType
指针的指针
这纯粹是运气,会导致未定义的行为,您不应该出于任何原因使用这种类型的代码。如果您需要内部指针,请在 unique_ptr 上使用 get()
方法。
【讨论】:
你是对的。我在几个在线编译器上测试了我的 SSCCE,它们的行为与我的 VS2015 完全相同,但最终它是 UB,应该避免。【参考方案3】:这是一种未定义的行为,因为唯一指针恰好只存储一个指针作为其状态,而该状态是指向T
的指针。
未定义的行为可以做任何事情,包括时间旅行和格式化硬盘。我知道有人这么说,也有人认为这是一个玩笑,但这些实际上是你可以通过实验验证的真实陈述。
碰巧的是,您在此处未定义的行为以“有效”的方式重新解释了一些记忆。
您不能使用您的库以定义的方式序列化/反序列化非 pod 结构。你可以破解它来工作,但是任何编译器更新(甚至是编译器标志更新!)都可能突然变得完全不同。
考虑有一个用于序列化/反序列化的结构,另一个用于运行时使用。马歇尔从一个到另一个。是的,这很糟糕。
【讨论】:
我的印象是这里的行为是unspecified,而不是undefined。 @Xirema 取消引用指向不是该对象的对象的指针?这会触发未定义的行为。我想一个特定的独特 ptr 实现可能是标准布局,其前缀与T*
兼容,并由编译器的标准库保证是这样的。以上是关于从 unique_ptr<T> 的 void* 转换为 T** 是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章
从 unique_ptr<T> 的 void* 转换为 T** 是如何工作的?
从 initializer_list 构造 std::map<T, unique_ptr<S>> 时出错