放置new和未初始化的POD成员

15

C++标准是否保证在使用placement new操作符后,未初始化的POD类型成员会保留其先前的值?

更准确地说,根据C++11规范,以下断言是否始终为真?

#include <cstdlib>
#include <cassert>

struct Foo {
    int alpha; // NOTE: Uninitialized
    int beta = 0;
};

int main()
{
    void* p = std::malloc(sizeof (Foo));
    int i = some_random_integer();
    static_cast<Foo*>(p)->alpha = i;
    new (p) Foo;
    assert(static_cast<Foo*>(p)->alpha == i);
}

对于C++03,答案是否相同?


1
我非常怀疑这一点。您还假设alphap具有相同的地址。如果Foo恰好是多态的,则绝对不是这种情况。 - Mysticial
1
严谨地说,未初始化可能意味着任何东西。编译器可以自由地暂时使用该内存插槽来做其他事情。更糟糕的是,整个变量可能在寄存器中,并且编译器决定为 p->alpha 切换寄存器 - 在这种情况下,它将取一个不同的值。 - Mysticial
2
@R.MartinhoFernandes:你可能是对的,但我的成员是POD类型,而标准针对POD类型有许多特殊情况,所以也许它确实被定义了... - Kristian Spangsege
1
顺便说一句,这是一个好问题。 - Lightness Races in Orbit
1
@n.m.:对我来说,new (p) Foo; 看起来相当像是放置 new。 - Lightness Races in Orbit
显示剩余12条评论
2个回答

20

C++标准是否保证在使用placement new后,未初始化的POD成员会保留其先前的值?

根据C++11,以下断言是否始终成立?

不是。

未初始化的数据成员具有“不确定”的值,这并不意味着基础内存保持不变。

: 创建类型为T的new表达式将按以下方式初始化该对象: - 如果省略了new-initializer,则对象将进行默认初始化(8.5); 如果没有执行初始化,则对象具有不确定的值。 - 否则,将根据直接初始化的8.5的初始化规则解释new-initializer。 : 默认初始化类型为T的对象意味着: - 如果T是(可能带有cv限定符的)类类型(Clause 9),则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化无效); - 如果T是数组类型,则每个元素都会进行默认初始化; - 否则,不执行初始化。 : 当使用odr创建其类类型的对象(1.8)或在其第一次声明后显式默认化时,默认情况下被默认并未定义为删除的默认构造函数在其被odr-used(3.2)时隐式定义。隐式定义的默认构造函数执行类的初始化集,这些初始化集将由一个空的compound-statement的user-written默认构造函数(12.6.2)而不是ctor-initializer(12.6.2)执行。 : 在非委托构造函数中,如果某个给定的非静态数据成员或基类没有被mem-initializer-id指定(包括没有mem-initializer-list的情况,因为构造函数没有ctor-initializer),并且该实体不是抽象类的虚基类(10.4),则: - 如果实体是具有brace-or-equal-initializer的非静态数据成员,则按8.5中指定的方式初始化该实体; - 否则,如果实体是变量成员(9.5),则不执行初始化; - 否则,对实体进行默认初始化(8.5)。

NB. 12.6.2/8中的第一个选项是如何处理您的成员beta的)

[C++11: 8.5/6]: 对于类型为T的对象进行默认初始化意味着:

  • 如果T是一个(可能带有cv限定符的)类类型(第9条),则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化无效);
  • 如果T是数组类型,则每个元素都进行默认初始化;
  • 否则,不执行任何初始化。

[C++11: 8.5/11]: 如果对象没有指定初始化器,则对象将进行默认初始化;如果没有执行初始化,则具有自动或动态存储期的对象具有不确定的值。

一个编译器在分配内存时可以选择清零(或以其他方式更改)底层内存。例如,Visual Studio 在调试模式下被知道将可识别的值(如0xDEADBEEF)写入内存以帮助调试;在这种情况下,你可能会看到他们用来表示“干净内存”的0xCDCDCDCDreference)。
在这种情况下,它会吗?我不知道。我不认为我们能知道。
我们所知道的是,C++ 不禁止它,我相信这就是这个答案的结论。 :)

这个答案对于C++03也是一样的吗?

是的,只不过逻辑稍有不同:

: 创建一个类型为T的new-expression,将初始化对象如下: - 如果省略了new-initializer: - 如果T是(可能是cv-qualified的)非POD类类型(或其数组),则对象进行默认初始化(8.5)。如果T是const限定类型,则底层类类型必须具有用户声明的默认构造函数。 - 否则,所创建的对象具有不确定的值。如果T是const限定类型,或者(可能是cv-qualified的)包含(直接或间接地)const限定类型成员的POD类类型(或其数组),则程序是非法的; - 如果new-initializer的形式为(),则该项进行值初始化(8.5); - 如果new-initializer的形式为(expression-list),并且T是类类型,则调用适当的构造函数,使用expression-list作为参数(8.5); - 如果new-initializer的形式为(expression-list),并且T是算术、枚举、指针或指向成员的类型,并且expression-list仅包含一个表达式,则对象将被初始化为表达式的(可能转换后的)值(8.5); - 否则,new-expression是非法的。
现在,这是我对初始化规则的严格解释。
实际上,从实践角度来看,我认为您可能正确地看到了与放置operator new语法定义的潜在冲突:
引用如下: “[C++11:18.6.1/3]:备注:有意不执行其他操作。”
随后的示例说明了放置new“可以用于在已知地址处构造对象”。
然而,它并没有讨论通常使用在已知地址处构造对象而不混淆先前存在的值的情况,但短语“不执行其他操作”确实表明其意图是您的“未确定值”应该是以前在内存中的任何内容。
或者,它只是禁止运算符本身采取任何操作,使分配器自由。对我来说,似乎标准试图传达的重要点是不会分配新的内存。
无论如何,访问此数据会导致未定义的行为:
[C++11: 4.1/1]: 非函数、非数组类型 T 的 glvalue (3.10) 可以转换为 prvalue。如果 T 是不完整类型,则需要此转换的程序是非法的。如果 glvalue 引用的对象不是类型 T 的对象,也不是派生自类型 T 的对象,或者对象未初始化,则需要此转换的程序具有未定义行为。如果 T 是非类类型,则 prvalue 的类型是 cv-qualified 版本的 T。否则,prvalue 的类型是 T。因此,实际上并不重要:你无论如何都不能合规地观察原始值。

@LightnessRacesinOrbit:你能在Visual Studio的调试模式下尝试我的示例吗?很可能只有实际分配器才执行可识别值的填充,该分配器不作为放置new的一部分运行。我假设你正在使用MSVC++。但我不是。 - Kristian Spangsege
1
operator new 可能有意不执行任何操作,但是仍然会发生新表达式的其余部分(即在5.3.4处使其不确定的业务)。 - R. Martinho Fernandes
不幸的是,仅仅因为operator new“不执行任何其他操作”这一事实是不足够的。请参阅此文档以了解原因:https://gcc.gnu.org/gcc-6/porting_to.html#flifetime-dse 简而言之,即使底层内存不会改变,对它的先前存储也可能被删除,因此它仍然不包含预期值。 - stsp
@stsp:就像我说的那样,这些值是未指定的,无论如何也无法观察到。感谢您提供了一个很好的实际例子,说明如果您想忽略这个禁令,可能会出现“问题”。 - Lightness Races in Orbit
是的,实际示例和一个实用的命令行选项可以避免这个问题(但clang中没有这样的选项!)。无论如何,我已经看到你通过其他方式得出了有效的结论。 - stsp
显示剩余5条评论

0

C++11 12.6.2/8 "初始化基类和成员" 规定:

在非委托构造函数中,如果某个非静态数据成员或基类没有被 mem-initializer-id 指定(包括构造函数没有 ctor-initializer 的情况),并且该实体不是抽象类的虚基类(10.4),则

  • 如果该实体是具有大括号或等号初始化器的非静态数据成员,则按照 8.5 中指定的方式进行初始化;
  • 否则,如果该实体是变体成员(9.5),则不执行任何初始化;
  • 否则,该实体将被默认初始化(8.5)。

对于 int 类型的默认初始化不会进行任何操作(8.5/6 "Initializers"):

默认初始化类型T的对象意味着:
  • 如果T是一个(可能带有cv限定符的)类类型(第9条),则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化无效);
  • 如果T是数组类型,则每个元素都会被默认初始化;
  • 否则,不执行任何初始化
因此成员变量alpha应该保持不变。

1
我认为将明确声明的“初始化”解释为“未指定的初始化”或类似的解释是牵强附会。请记住,标准特别指出:“显式放置和销毁对象的这种用法可能是应对专用硬件资源和编写内存管理设施所必需的。” 我认为处理硬件资源可能是“无初始化”承诺可能具有意义和重要性的领域之一。 - Michael Burr
1
@LightnessRacesinOrbit:我不确定我能接受你的论点。我也一直在研究这个标准。我会尝试稍后提供详细的论据,但现在,请查看18.6.1.3节。放置版本明确说明它们什么都不做,并且不能被应用程序覆盖。再加上未初始化POD成员的初始化明确被跳过的事实。 - Kristian Spangsege
更重要的是,@KristianSpangsege 18.6.1.3 是不相关的。new 不是 operator new - R. Martinho Fernandes
@SteveJessop:同意。我应该使用“可信”的词语,而不是“合理假设”。 - Kristian Spangsege
1
@KristianSpangsege:是的,如果标准旨在这样做但未能将其写入文档,则需要进行缺陷报告和更正(前提是它不会比委员会愿意的现有实现更糟)。我仍然有一半的希望,在这次讨论中还没有提出拯救这一天的条款;-) - Steve Jessop
显示剩余21条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接