初始化表达式可以使用变量本身吗?

Rus*_*lan 17 c++ initialization undefined-behavior language-lawyer

请考虑以下代码:

#include <iostream>

struct Data
{
    int x, y;
};

Data fill(Data& data)
{
    data.x=3;
    data.y=6;
    return data;
}

int main()
{
    Data d=fill(d);
    std::cout << "x=" << d.x << ", y=" << d.y << "\n";
}
Run Code Online (Sandbox Code Playgroud)

d是从返回值复制初始化的fill(),但在返回结果之前fill()写入d自身.我关心的是d在初始化之前非常简单地使用它,并且在一些(所有?)情况下使用未初始化的变量会导致不确定的行为.

那么这段代码是有效的,还是有未定义的行为?如果它有效,一旦Data停止POD或在其他情况下,行为是否会变得不确定?

Sha*_*our 13

这似乎不是有效的代码.它类似于问题中概述的情况:将C++对象传递给自己的构造函数是否合法?,虽然在这种情况下代码是有效的.机制并不完全相同,但基本推理至少可以让我们开始.

我们从缺陷报告363开始,询问:

如果是这样,UDT的自我初始化的语义是什么?例如

 #include <stdio.h>

 struct A {
        A()           { printf("A::A() %p\n",            this);     }
        A(const A& a) { printf("A::A(const A&) %p %p\n", this, &a); }
        ~A()          { printf("A::~A() %p\n",           this);     }
 };

 int main()
 {
  A a=a;
 }
Run Code Online (Sandbox Code Playgroud)

可以编译和打印:

A::A(const A&) 0253FDD8 0253FDD8
A::~A() 0253FDD8
Run Code Online (Sandbox Code Playgroud)

拟议的决议是:

3.8 [basic.life]第6段表明此处的引用是有效的.允许在完全初始化之前获取类对象的地址,并且只要引用可以直接绑定,就允许将它作为参数传递给引用参数.[...]

因此,尽管d未完全初始化,我们可以将其作为参考传递.

我们开始遇到麻烦的地方在这里:

data.x=3;
Run Code Online (Sandbox Code Playgroud)

草案C++标准部分3.8(缺陷报告引用的相同部分和段落)说(强调我的):

类似地,在对象的生命周期开始之前但在对象将占用的存储已经被分配之后,或者在对象的生命周期结束之后并且在对象占用的存储被重用或释放之前,任何引用的glvalue之前可以使用原始对象,但仅限于有限的方式.对于正在建造或销毁的物体,见12.7.否则,这样的glvalue指的是已分配的存储(3.7.4.2),并且使用不依赖于其值的glvalue的属性是明确定义的.如果出现以下情况,该程

  • 左值到右值的转换(4.1)应用于这样的glvalue,

  • glvalue用于访问非静态数据成员或调用对象的非静态成员函数,或

  • glvalue绑定到对虚基类(8.5.3)的引用,或

  • glvalue用作dynamic_cast(5.2.7)的操作数或typeid的操作数.

那么访问意味着什么?通过缺陷报告1531澄清了这一点,该报告将访问定义为:

访问

读取或修改对象的值

因此fill 访问非静态数据成员,因此我们有未定义的行为.

这也符合以下部分12.7:

[...]要形成指向对象obj的直接非静态成员(或访问其值)的指针,obj的构造应该已经开始并且其销毁不应该已经完成​​,否则计算指针值(或访问成员值)导致未定义的行为.

因为您无论如何都在使用副本,所以您也可以在其中创建一个Data实例fill并对其进行初始化.你不必通过d.

正如TC指出的那样,明确引用生命周期开始时的详细信息非常重要.来自部分3.8:

对象的生命周期是对象的运行时属性.如果一个对象属于类或聚合类型,并且它或其成员之一由除了普通默认构造函数之外的构造函数初始化,则称该对象具有非平凡的初始化.[注意:通过简单的复制/移动构造函数进行初始化是非平凡的初始化. - 结束注释]类型T对象的生命周期从以下开始:

  • 获得具有适当对齐和T型尺寸的存储,并且

  • 如果对象具有非平凡的初始化,则其初始化完成.

由于我们通过复制构造函数初始化,因此初始化非常重要.

  • @MM正如我所看到的,在构造函数开始执行之前,对象不在构造之中.此处的访问在此之前发生,同时评估要传递给构造函数的参数. (2认同)
  • 我同意你的结论.我还要指出,如果x和y是某种非POD类型,并且赋值运算符依赖于现有值,那么调用该赋值运算符(就像在fill中一样)必然会失败(因为x和y避难所)还没有建成.鉴于它不适用于复杂的类,我对使用它与POD类感到非常紧张.(即使标准*是*允许它,这种边缘情况是编译器错误可能潜伏的地方.) (2认同)

MSa*_*ers 2

我不认为有什么问题。访问未初始化的整数成员是有效的,因为您访问的目的是为了写入。阅读它们会导致UB。

  • 您能否评论一下为什么沙菲克的答案是错误的,因为它似乎与您的答案相矛盾? (2认同)