从 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>> 时出错

从函数返回 unique_ptr

从函数返回 unique_ptr

unique_ptr<T> 和 unique_ptr<T>&& 之间的区别 [重复]

两个 unique_ptr<T> 的无锁交换