是否保证在定义变量时确切地调用构造函数?

时间:2016-09-06 03:20:53

标签: c++ c++11 constructor standards undefined-behavior

请考虑以下代码:

#include <cstdio>

struct A {
  A() {
    printf("Bar\n");
  }
};

int main() {
  printf("Foo\n");
  A a;
  return 0;
}

是否可以保证按顺序打印Foo\nBar\n?我的经验是&#34; yes&#34;,但是我想引用C ++ 11标准或MSDN引用的一些引用,因为有人告诉我,编译器可以在之前调用构造函数。实际的宣言。

如果构造函数有一些参数(因为它们可以依赖于函数启动时未计算的值),那将更加明显,如果只有默认构造函数则不太明显。比方说,JavaScript以在var行可用之前定义变量而闻名:

function main() {
  console.log(x);
  var x = 2;
  console.log(y);
}
main();

上面的代码会将undefined打印为x的值,然后以y is not defined失败。

5 个答案:

答案 0 :(得分:7)

  

[intro.execution] / 10 full-expression 是一个表达式,它不是另一个表达式的子表达式。 [注意:在某些情况下,   例如未评估的操作数,句法子表达式被认为是完整表达式(第5条)。 末端   note ]如果定义了一个语言结构来产生函数的隐式调用,则使用该语言   构造被认为是用于该定义目的的表达式。

  

[intro.execution] / 14 在与每个要计算的下一个完整表达式相关的值计算和副作用之前,每个与全表达式相关的值计算和副作用都会被排序。 / p>

答案 1 :(得分:6)

  

因为有人告诉我,编译器可以在实际的声明行之前调用构造函数。

编译器只有在能够证明程序的可观察行为不会因此而改变时才能自由地执行此操作。在你的情况下,这是不正确的。因此,符合标准的编译器不会在声明行之前调用构造函数。

答案 2 :(得分:5)

您可以保证此程序将输出Foo\nBar\n

但是,这就是你所保证的。这是您的程序将显示的行为,但是as-if rule,您无法保证 该行为的完成情况。您无法保证何时调用构造函数,甚至如果调用构造函数。您的程序的可执行代码甚至可能根本没有构造函数!你甚至不能保证变量a存在于程序的某个地方。

这不是理论上的;如果您的编译器完全有价值,那么启用优化后,您的程序应该编译为完全与程序相同

#include <cstdio>
int main() {
    std::printf("Foo\n");
    std::printf("Bar\n");
    return 0;
}

答案 3 :(得分:1)

关于具有副作用的构造函数或析构函数的类型的自动变量有一个特殊规则,例如此代码的a:即使它显然未被使用,也无法优化这样的变量。

在C ++ 14中(我使用的是N3936草案),这是

C ++14§3.7.3/ 3(basic.stc.auto/3):
  

如果具有自动存储持续时间的变量具有初始化或具有副作用的析构函数,则不应该   在其块结束之前被销毁,即使它似乎未被使用也不会作为优化被删除,除非可以按照12.8中的规定消除类对象或其复制/移动。

C ++ 03中的措辞相同。

嗯,标准的好处在于它通常不是100%明确,为讨论留下了空间!在这里我们可以阅读“(带有副作用的初始化或析构函数)”,或“初始化或(带有副作用的析构函数)”。思考什么是合理,这显然是第一个解释的意图。

RE

  

是否可以保证按顺序打印Foo\nBar\n

C ++中的优化被限制为产生与源代码的直接含义相同的可见效果,称为“as-if”规则

C ++14§1.9/ 1(intro.execution / 1):
  

本国际标准中的语义描述定义了参数化的非确定性摘要   机。本国际标准对符合实施的结构没有要求。特别是,它们不需要复制或模拟抽象机器的结构。相反,需要符合实现来模拟(仅)抽象机器的可观察行为,如下所述。

以下段落详细说明了这意味着什么,例如:所有投注均在图片中以未定义的行为结束。

在上一段末尾有一个非规范的脚注:

  

此条款有时被称为”as-if“规则,因为只要结果就像要求已被遵守,实施可以自由地忽略本国际标准的任何要求,至于可以从程序的可观察行为中确定。例如,实际实现不需要评估表达式的一部分,如果它可以推断出它的值没有被使用,并且没有产生影响程序的可观察行为的副作用。

然后,不需要优化,这里没有UB!只需要考虑源的定义效果。

Igor's answer所述,这些定义的效果由两个完整表达式产生,其中标准保证第一个效果在第二个之前完成被执行。

在其他新闻中:

  • return 0;中的main不需要,因为这是main的默认设置。

  • 该程序正式无法移植,因为<cstdio>无法保证将printf放在全局命名空间中。请改为包含<stdio.h>。这不是同名的C头:它是一个C ++头,与<cstdio>具有相同的效果,除了保证使用哪些命名空间。

答案 4 :(得分:0)

简而言之:可能有一些规则允许编译器忽视构造或简单地“做出不会有任何可观察到的差异的事情”。 - 但是,对你有用的是对你无论如何都能观察到的事情的了解?

唯一可以产生差异的情况是全局变量,但它的故事有点不同 - 全局变量没有定义初始化顺序。函数本地的变量总是&#34;运行时变量&#34;,这意味着它们在声明之前实际上不存在。此外,您可以使用先前在参数中声明的变量来初始化稍后声明的变量 - 因此必须至少在这种情况下确保初​​始化顺序。

编译器可以同时对所有变量进行堆栈分配,然后根据需要填写所有变量。但是,局部变量无论如何都将被运行时初始化。我可以想象你可能会在这种情况下观察重新排序的初始化:

int a = 0, b = 0;
A ax;
int c = 0, d = 0;

可能会发生编译器决定为所有这些变量分配单个堆栈帧并清除它们下面的整个内存区域 - 我怀疑任何编译器都这样做,但至少这样做并没有错。在这种情况下,您可能会在调用A :: A()之前观察到c包含零。

这可以做到,因为0到c的赋值是独立于运行时的,所以它可以比它看起来更早完成。但是,理论上这个变量在调用A :: A()之前不应该存在,所以从语言的角度来看,真正清除的是一些堆栈存储器尚未分配给变量的符号。

如果你要求任何可观察到的副作用 - 是的,保证局部变量初始化的所有可观察结果(不仅是副作用)都是完全按照它们被声明的顺序构造变量的时候(此外,它们正在被破坏&#34;以相反的顺序)。