【问题标题】:Can initializing expression use the variable itself?初始化表达式可以使用变量本身吗?
【发布时间】:2016-02-12 11:23:15
【问题描述】:

考虑以下代码:

#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";
}

这里d 是从fill() 的返回值复制初始化的,但fill() 在返回其结果之前写入d 本身。我担心的是 d 在被初始化之前是非平凡使用的,并且在某些(所有?)情况下使用未初始化的变量会导致未定义的行为。

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

【问题讨论】:

标签: c++ initialization language-lawyer undefined-behavior


【解决方案1】:

这似乎不是有效的代码。它类似于问题中概述的情况:Is passing a C++ object into its own constructor legal?,尽管在这种情况下代码是有效的。机制并不相同,但基本推理至少可以让我们开始。

我们从defect report 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;
 }

可以编译打印:

A::A(const A&) 0253FDD8 0253FDD8
A::~A() 0253FDD8

提议的决议是:

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

所以虽然d 没有完全初始化,但我们可以将它作为引用传递。

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

data.x=3;

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

类似地,在对象的生命周期开始之前但在对象的生命周期开始之后 对象将占用的存储空间已被分配,或者在 对象的生命周期已经结束并且在存储之前 被占用的对象被重用或释放,任何引用 可以使用原始对象,但只能以有限的方式使用。对于一个对象 正在建造或毁坏,见 12.7。否则,这样的glvalue 指分配的存储(3.7.4.2),并使用 不依赖于其值的 glvalue 是明确定义的。该程序 在以下情况下具有未定义的行为:

  • 将左值到右值的转换 (4.1) 应用于此类左值,

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

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

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

那么访问是什么意思呢? defect report 1531 澄清了这一点,它将访问定义为:

访问

读取或修改对象的值

所以fill 访问一个非静态数据成员,因此我们有未定义的行为。

这也与12.7 部分一致:

[...]形成一个指向(或 访问对象 obj 的直接非静态成员的值,则应开始构建 obj 并且它的销毁应该没有完成,否则指针值的计算(或访问 成员值)导致未定义的行为。

由于您无论如何都在使用副本,因此您不妨在 fill 内创建一个 Data 实例并对其进行初始化。您避免必须通过d

正如 T.C. 指出的那样。明确引用生命周期何时开始的细节很重要。来自3.8部分:

对象的生命周期是对象的运行时属性。一个 如果对象属于一个类,则称该对象具有非平凡的初始化 或聚合类型,并且它或其成员之一由 构造函数而不是普通的默认构造函数。 [ 笔记: 由平凡的复制/移动构造函数初始化是不平凡的 初始化。 — 尾注] T 类型对象的生命周期 开始时间:

  • 获得了适合类型 T 的对齐方式和大小的存储,并且

  • 如果对象有非平凡的初始化,它的初始化就完成了。

初始化很重要,因为我们是通过复制构造函数进行初始化的。

【讨论】:

  • 这缺少 3.8/1 中关于 d 的生命周期开始时的引用。
  • @T.C.公平点,我通过参考缺陷报告隐含了它,但我应该明确表示。不过,我暂时无法对其进行编辑。
  • @M.M 我不清楚,我认为它们都适用。具有类似情况的缺陷报告引用了3.8,但3.8 引用了12.7,但似乎他们以不同的方式说同样的话。似乎需要对措辞进行一些修正。
  • @M.M 如我所见,对象在其构造函数开始执行之前并未在构造中。此处的访问发生在此之前,同时评估要传递给构造函数的参数。
  • 我同意你的结论。我还要指出,如果 x 和 y 属于某些非 POD 类型,其赋值运算符依赖于现有值,那么调用该赋值运算符(如您在填充中所做的那样)必然会失败(因为 x 和 y 没有'尚未建造)。鉴于它不能与复杂的类一起使用,我会对将它与 POD 类一起使用感到非常紧张。 (即使标准允许,这种边缘情况也是编译器错误可能潜伏的地方。)
【解决方案2】:

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

【讨论】:

  • 您能否评论一下为什么 Shafik 的答案是错误的,因为它似乎与您的答案相矛盾?
  • @Ruslan:省略 Standardese,this-&gt;x = 3Data 的构造函数中是合法的,即使 x 没有被初始化列表初始化。这取决于x 没有(默认)构造函数,因为它是一个普通的int,因为对于具有构造函数的成员来说,此时将被初始化。从根本上说,“寿命”对于 POD 来说是一个模糊的问题。 *(int*) malloc(sizeof(int)) = 5 必须对 C 兼容有效,这表明存储空间是足够的。这反映了 Shaik 在 3.8 中的最后一句话。 Data::x 有正确的存储和简单的初始化,所以写入是可以的。
【解决方案3】:

我认为它是有效的(疯狂,但有效)。

这既合法又合乎逻辑:

Data d ;

d = fill( d ) ;

事实上这种形式是一样的:

Data d = fill( d ) ;

就语言的逻辑结构而言,这两个版本是等价的。

所以它对语言来说是合法且逻辑正确的。

但是,由于我们通常希望人们在创建变量时将它们初始化为默认值(为了安全起见),这是糟糕的编程习惯

有趣的是,g++ -Wall 编译这段代码时不费吹灰之力。

【讨论】:

  • 这些形式并不相同,并且“就语言的逻辑结构而言”它们并不等同。一种是初始化,一种是赋值。此外,语言律师问题的答案应包括对语言标准的引用以支持所提出的论点。
  • 好吧,你错了。
  • 因为 C++ 标准说它是初始化。这真的是基本的东西。 Here is an intro,请阅读 C++ 标准第 8.5.4 节以获取完整描述。你在这里超出了你的深度。
  • “事实上,变量 d 必须在函数结果产生之前被初始化(即使它带有垃圾)。” - 不,它没有。这就是这个问题的重点,fill(d) 在初始化之前就使用了d。有关支持证据,请参阅 Shafik 的回答。
  • 顺便说一句,如果您要说“至少有共同的礼貌来说明为什么 [downvote]”之类的话,那么您应该接受解释。可能人们不会解释他们的反对意见,因为他们不想陷入像这样的蹩脚“争论”,你只是坚持自己一直都是对的,不听。
猜你喜欢
  • 2023-02-01
  • 2011-01-21
  • 1970-01-01
  • 2013-05-28
  • 2019-10-23
  • 1970-01-01
  • 2011-01-21
相关资源
最近更新 更多