【问题标题】:Placement new base subobject of derived in C++在 C++ 中放置派生的新基础子对象
【发布时间】:2019-03-23 20:48:39
【问题描述】:

它是否定义了行为来放置新的派生的可简单破坏的基础对象?

struct base { int& ref; };
struct derived : public base {
    complicated_object complicated;
    derived(int& r, complicated_arg arg) :
            base {r}, complicated(arg) {}
};

unique_ptr<derived> rebind_ref(unique_ptr<derived>&& ptr,
                               int& ref) {
    // Change where the `ref` in the `base` subobject of
    // derived refers.
    return unique_ptr<derived>(static_cast<derived*>(
        ::new (static_cast<base*>(ptr.release()) base{ref}));
}

请注意,我尝试构造 rebind_ref 以不破坏编译器可能做出的任何严格的别名假设。

【问题讨论】:

  • 在这种情况下为什么不使用int *ref。假设 ref 永远不会改变,可以编写派生类。
  • 我的问题不是重新绑定参考。它是关于在派生的基础子对象之上构造一个“新”值。
  • 重用内存会结束对象的生命周期。

标签: c++ inheritance strict-aliasing object-lifetime placement-new


【解决方案1】:

不,C++ 标准不允许这样做,至少有两个原因。

[basic.life], paragraph 8 中可以找到有时允许将新对象放置到存储中以存储另一个相同类型的文本的文本。大胆的强调是我的。

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,如果:

  • 新对象的存储空间正好覆盖了原始对象占用的存储位置,并且

  • 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且

  • 原始对象的类型不是 const 限定的,并且,如果是类类型,不包含任何类型为 const 限定或 引用的非静态数据成员输入

  • [C++17] 原始对象是T 类型的派生度最高的对象,而新对象是T 类型的派生度最高的对象(也就是说,它们是不是基类子对象)。

  • [C++20 草案 2018-10-09] 原始对象和新对象都不是可能重叠的子对象([intro.object])。

C++20 的变化是考虑到零大小非静态数据成员的可能性,但它仍然排除了所有基类子对象(空或非空)。 “可能重叠的子对象”是[intro.object] paragraph 7 中定义的一个新术语:

可能重叠的子对象是:

  • 基类子对象,或

  • 使用 no_unique_address 属性 ([dcl.attr.nouniqueaddr]) 声明的非静态数据成员。

(即使您确实找到了一些方法来重新安排事物以避免引用成员和基类问题,请记住确保没有人可以定义 const derived 变量,例如将所有构造函数设为私有!)

【讨论】:

  • 感谢您的出色回答。 “...指向原始对象的指针、引用原始对象的引用或原始对象的名称将自动引用新对象……”很有趣。 Placement-new 表达式的结果是“指向原始对象的指针”,还是指向新构造对象的指针(尽管它占用相同的内存位置)?
  • 任何 new 表达式的结果,包括放置 new,指向创建的对象。那句话是在谈论以前存在的指针和引用。
  • 我仔细构造了rebind_ref,使它使用“旧”指针并将作为放置新表达式static_casted 到derived 的结果的“新”指针返回。这足以避免“指向原始对象的指针”规则吗?
  • @Filipp 基类子对象并不是真正的“指针”、“引用”或“名称”。但是标准不需要阐明派生对象在替换基类子对象后的行为方式,因为它说这样的替换首先总是无效的。
  • 实际上,您在上面引用的标准部分仅说明了是否可以使用引用先前占用一些存储空间的对象的“指针”、“引用”或“名称”来操作在那里创建的新对象。在 C++17 中,我会使用 std::launder 来“创建”一个“新名称”。不幸的是,我只能访问 C++14。在 C++17 之前,placement new 是否具有 std::launder 权限?
【解决方案2】:
static_cast<derived*>(
        ::new (&something) base{ref})

无效,根据定义 new (...) base(...) 创建一个 base 对象作为新的完整对象,有时可以将其视为现有的完整对象或 成员 子对象(在不是无论如何,base 遇到过),但从来不是基础子对象。

没有现有规则说您可以假装 new (addr) base 创建一个有效的派生对象,只是因为 base 对象正在覆盖另一个 base 基础子对象。如果之前有一个derived 对象,则您刚刚使用new (addr) base 重用了它的存储空间。即使通过某种魔法derived 对象仍然存在,新表达式的评估结果也不会指向它,它会指向一个base 完整对象。

如果你想假装你做了某事(比如创建一个derived 对象),而不是实际做它(调用derived 构造函数),你可以在指针上注入一些volatile 限定符以强制编译器擦除所有关于值的假设并编译代码,就好像有 ABI 转换一样。

【讨论】:

  • 你会建议我在哪里添加 volatile 限定符以使我正在做的事情不是 UB 而不会对编译器如何优化此代码产生负面影响?
  • @Filipp volatile 的目标是从编译器中删除信息:volatile 对象的值似乎来自单独编译的单元(并且可能来自不同的编译器和语言) . 该标准根本不涵盖单独编译或混合不同语言,甚至 C。
猜你喜欢
  • 2017-02-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-07
相关资源
最近更新 更多