g++ -Wreorder的意义是什么?

181

g++ 的 -Wall 选项包含 -Wreorder。该选项的作用如下所述。我不明白为什么有人会关心这个问题(特别是足够关心将其默认打开在 -Wall 中)。

-Wreorder (仅限 C++)
警告当代码中给出成员初始化程序的顺序与执行它们的顺序不匹配时。例如:
struct A { int i; int j; A(): j(0), i(1) { } };
编译器将重新排列 i 和 j 的成员初始化程序以匹配成员的声明顺序,并发出警告。此警告已启用 -Wall。

4
这里有一些不错的答案,但是如果有人感兴趣的话,简单提一下:g++有一个标志可以将这个问题视为一个完整的错误:-Werror=reorder。 - Max Barraclough
6个回答

295

考虑:

struct A {
    int i;
    int j;
    A() : j(0), i(j) { }
};

现在,i被初始化为某个未知值,而不是零。

或者,i的初始化可能具有某些副作用,其顺序很重要。例如:

A(int n) : j(n++), i(n++) { }

97
这个例子真的应该放在文档中作为示例。 - Ben S
3
多数类型都是带有简单初始化器的POD类型,所以我没有想到这一点。你的例子比g++手册中的例子要好得多。谢谢。 - Peeter Joot
5
@Mike,这是因为你的编译器(gcc)将未初始化的变量初始化为0,但这不是你应该依赖的;i为0只是未初始化变量的未知值为0的副作用。 - ethanwu10
3
@KymikoLoco说的完全错误。man手册中的例子是来自于OP(其中i初始化为1)。而这里,i被初始化为j,实际上展示了一个问题。 - jazzpi
2
在这段代码片段中,为什么i会被初始化为一个未知的值? - Nubcake
显示剩余7条评论

57
问题在于有人可能会看到构造函数中的成员初始化列表,并认为它们按照那个顺序执行(首先是j,然后是i)。但实际上不是这样,它们按照类中定义的成员顺序执行。
假设你写了 A(): j(0), i(j) {}。有人可能会读到这个代码并认为i最终得到的值是0。但事实上它不是,因为你用j来初始化i,而j本身包含垃圾值,因为它尚未被初始化。
警告提醒你编写A(): i(j), j(0) {},希望这看起来更可疑一些。

看起来/闻起来确实有点可疑! :) 绝对是代码异味 :) 感谢您清晰明了的解释。 :) - Will
1
"...提醒您编写A():i(j),j(0) {}。我建议在这种情况下重新排列类成员。" - 2.718

30

其他答案已经提供了一些很好的例子,证明了警告选项的必要性。我想提供一些历史背景。C++的创造者Bjarne Stroustrup在他的书The C++ programming language(第三版,第259页)中解释:

在执行包含类自己的构造函数之前,成员构造函数就已经被调用。这些构造函数按照它们在类中声明的顺序调用,而不是按照它们在初始化列表中出现的顺序。为了避免混淆,最好按照声明顺序指定初始化程序。成员析构函数按照相反的构造顺序调用。


1
这是第一个展示如何正确修复问题的答案。 - mafu

13

如果你的初始化器具有副作用,这可能会对你造成影响。请考虑以下示例:

int foo() {
    puts("foo");
    return 1;
}

int bar() {
    puts("bar");
    return 2;
}

struct baz {
    int x, y;
    baz() : y(foo()), x(bar()) {}
};

尽管直觉上人们认为按照初始化列表中的顺序进行初始化,但上述代码将打印出"bar"然后是"foo"。

或者,如果xy是一些具有构造函数的用户定义类型,则该构造函数也可能具有副作用,并得到同样不明显的结果。

这种情况还可能出现在一个成员变量的初始化引用了另一个成员变量的情况下。


8
警告存在的原因是,如果你只看构造函数,会认为ji之前被初始化。如果一个变量用来初始化另一个变量,这就成了问题,例如:
struct A {
  int i;
  int j;
  A(): j (0), i (this->j) { }
};

从构造函数的外观来看,这似乎很安全。但实际上,在用于初始化i的时候,j尚未被初始化,因此代码不会按预期工作。因此出现了警告。


1

已经有很多好的回答了,但是还有一件事情没有提到 - 为什么初始化顺序是这样的?

类成员的初始化顺序不是构造函数中初始化器列表中列出的顺序,而是它们在类本身中声明的顺序。原因有点微妙。

C++保证成员变量的销毁顺序与构造顺序相反。由于一个类可以有多个具有不同初始化器列表的构造函数,析构函数需要知道这个顺序,这将导致繁重的簿记问题和不必要的开销。解决方案是强制每个构造函数对成员使用相同的初始化顺序,以便销毁顺序也能固定。

由于无法使用初始化器顺序,唯一的选择就是使用声明顺序。

尽管C++以不同于初始化器列表的顺序初始化变量完全合法,但这可能导致非直观的错误源。该警告让你知道要注意这些错误,或者重新调整初始化器列表以反映实际情况。


有一次在面试中,有人问我关于初始化顺序的问题,结果我答错了。我向面试官请教了解释,并且那天学到了一些东西。现在是时候将这个知识传递出去了。 - Mark Ransom
有一次在面试中,有人问我关于初始化顺序的问题,我回答错了。我向面试官请教了解释,并在那天学到了一些东西。现在是时候把它传递给别人了。 - undefined

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