该代码段在 Colliru 中编译时带有警告,但在 Ideone 中正常编译.哪一个是正确的

The snippet compiles with warnings in Coliru, but compiles normally in Ideone. Which one is correct?

本文关键字:编译 常编译 哪一个 Ideone Colliru 代码 警告 但在      更新时间:2023-10-16

此代码在 Coliru 中编译,并带有警告 [main()std::cout <<表达式中的单位化成员a[1].ia[2].i],但在 Ideone 中正常编译。

#include <iostream>
struct A
{
int i;
A(int j) : i{j} {};
A() = default;
};
int main() {
A a[3] = { A(1) };
std::cout << a[1].i << ' ' << a[2].i << 'n';
}

根据我对 iso § 8.5 p7 的解释,Ideone 是正确的,因为该条款的第 4 个要点。

这是 N8.5 中的 § 3797 p7

对 T 类型的对象进行值初始化意味着:

  • 如果 T 是一个(可能符合 cv 条件的)类类型(条款 9),没有默认构造函数 (12.1) 或默认构造函数 用户提供或删除,则对象默认初始化;
  • 如果 T 是没有用户提供或删除的默认构造函数的(可能符合 cv 条件的)类类型,则该对象为零初始化 并检查默认初始化的语义约束, 如果 T 有一个非平凡的默认构造函数,则对象为 默认初始化;
  • 如果 T 是数组类型,则每个元素都是值初始化的;
  • 否则,对象初始化为零。

值初始化的对象被视为构造的,并且 因此,本标准的规定适用于 "构造的"对象,构造函数具有的对象 完成"等,即使没有为对象的 初始化。

就C++14(N3797)而言,Ideone是正确的(参见Casey对C++11的回答),因为a[1]a[2]是用A{}初始化的,这是值初始化,导致i为0。这来自 N3797 § 8.5.1/7:

如果列表中的初始值设定项子

句少于聚合中的成员数,则应从其大括号或等于初始值设定项初始化每个成员,如果没有大括号或等于初始值设定项,则应从空初始值设定项列表 (8.5.4) 初始化。[ 示例:

struct S { int a; const char* b; int c; int d = b[a]; };
S ss = { 1, "asdf" };
使用 1 初始化 ss.a

,使用 "asdf" 初始化 ss.b,使用 int{} 形式的表达式的值初始化 ss.c(即 0),使用 ss.b[ss.a](即"s")的值初始化 ss.d

数组是 § 8.5.1/1 (An aggregate is an array...的聚合,因此这适用于数组的初始化。

表达式T{}(空初始值设定项列表)值根据 § 8.5.4/3 初始化对象:

否则,如果初始值设定项列表没有元素,并且 T 是具有默认构造函数的类类型,则对象将进行值初始化。

我们可以确认值初始化使i的值为 0,§ 8.5/8:

如果 T 是没有用户提供或删除的默认构造函数的(可能符合 cv 条件的)类类型,则对象为零初始化并检查默认初始化的语义约束,如果 T 具有非平凡的默认构造函数,则对象为默认初始化;

您的类没有用户根据 § 8.4.2/4 提供的默认构造函数:

如果函数是用户

声明的并且未显式默认,则函数是用户提供的,或者 在其第一次声明中删除。

需要注意的有趣一点是,如果您的默认构造函数是用户提供的并且没有初始化i,则将使用 8.5/8 的第一点,i将保持未初始化状态,如本例所示。

最后,关于比较的小说明。Ideone使用几个不同版本的GCC。使用哪一个会有所不同(如果需要,您可以与__VERSION__联系)。在这种情况下,编译器标志也略有不同。如果存在-std=c++1y(除了使用附加功能之外,我不知道检查的方法),将有一些 C++14 支持,但不是完整的支持,所以小的变化(T{}vs.T(),从大括号或等于初始值设定项初始化并检查默认初始化的语义约束)可能无法实现。实际上,您甚至可以检查第一个。Coliru 允许您配置构建命令,因此仅说 Coliru 是非常模棱两可的。

无论哪种方式,在获得足够的 C++14 支持(或至少在标准化之前)之前,使用 N3797 测试一致性行为都不太值得。我倾向于坚持使用 N3485,直到发生这种情况。在这个具体的例子中,我认为这两个标准的行为之间没有任何区别。查看凯西的答案,了解这两个标准在这件事上的区别。您有一个转换构造函数,因此您的对象将在 C++11 中默认初始化。

对于同时具有默认构造函数和另一个非默认构造函数的类,C++11 和 N3797 之间的值初始化行为存在明显差异。C++11 § 8.5/7:

对 T 类型的对象进行值初始化意味着:

  • 如果 T 是具有用户提供的构造函数 (12.1) 的(可能符合 cv 条件的)类类型(条款 9),则 调用 T 的默认构造函数(如果 T 没有可访问的默认值,则初始化格式不正确) 构造函数);
  • 如果 T 是没有用户提供的构造函数的(可能符合 cv 条件的)非联合类类型,则对象 为零初始化,如果 T 隐式声明的默认构造函数不平凡,则该构造函数为 叫。
  • 如果 T 是数组类型,则每个元素都是值初始化的;
  • 否则,对象初始化为零。

N3797 § 8.5/8:

对 T 类型的对象进行值初始化意味着:

  • 如果 T 是(可能符合 cv 条件的)类类型(条款 9),没有默认构造函数 (12.1) 或 默认构造函数是用户提供或删除的,则对象是默认初始化的;
  • 如果 T 是没有用户提供或删除的默认构造函数的(可能符合 cv 条件的)类类型,则 对象初始化为零,并检查默认初始化的语义约束,如果 T 有一个非平凡的默认构造函数,对象是默认初始化的;
  • 如果 T 是数组类型,则每个元素都是值初始化的;
  • 否则,对象初始化为零。

您的struct A具有用户声明的默认构造函数A() = default;和用户提供的非默认构造函数A(int j) : i{j} {}。在 C++11 中,它受制于第一个项目符号:它有一个用户提供的构造函数,因此调用默认构造函数(它不执行任何操作:A的默认构造函数是微不足道的)。在 N3797 中,第二个项目符号适用,因为A"没有用户提供或删除的默认构造函数",因此对象初始化为零。

简而言之,在具有任何用户提供的构造函数的类对象的 C++11 中进行值初始化不会在默认初始化之前执行零初始化。在 N3797 中,没有用户提供的默认构造函数的类对象的值初始化将在默认初始化之前执行零初始化。

看来 Coliru 上的 clang 版本在 C++11 之后一直在跟踪这里的标准,但 GCC 4.8 没有。

编辑:该测试程序表明GCC 4.8实际上确实遵循N3797规则进行值初始化。问题似乎是它默认初始化未提供初始值设定项的数组元素,而不是按照标准的要求对它们进行初始化。请注意第二个数组元素(显式提供空初始值设定项)与第三个数组元素(未提供初始值设定项)之间的行为差异。

这看起来像一个可能的 GCC 错误。

编辑:由Ideone上的相同GCC版本编译的相同测试程序没有演示该错误。不知道这里发生了什么。也许不同的编译器标志会影响 Ideone 上的输出,我不知道如何确定使用的编译器命令行。

Coliru 的默认编译命令是:

g++-4.8 -std=c++11 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

它使用-Wall,可以启用更广泛的警告。为了迂腐,它启用了以下警告:

-Waddress   
-Warray-bounds (only with -O2)  
-Wc++11-compat  
-Wchar-subscripts  
-Wenum-compare (in C/ObjC; this is on by default in C++) 
-Wimplicit-int (C and Objective-C only) 
-Wimplicit-function-declaration (C and Objective-C only) 
-Wcomment  
-Wformat   
-Wmain (only for C/ObjC and unless -ffreestanding)  
-Wmaybe-uninitialized 
-Wmissing-braces (only for C/ObjC) 
-Wnonnull  
-Wopenmp-simd 
-Wparentheses  
-Wpointer-sign  
-Wreorder   
-Wreturn-type  
-Wsequence-point  
-Wsign-compare (only in C++)  
-Wstrict-aliasing  
-Wstrict-overflow=1  
-Wswitch  
-Wtrigraphs  
-Wuninitialized  
-Wunknown-pragmas  
-Wunused-function  
-Wunused-label     
-Wunused-value     
-Wunused-variable  
-Wvolatile-register-var 

有关这些的更多信息可以在这里找到。

尽管Ideone也使用GCC 4.8,但它很可能没有设置-Wall。如您所见,没有那面旗帜,科里鲁也不会发出警告。

明确地说:它们实际上是相同的编译器,并且具有相同的标志,它们的行为完全相同,因此它们都是正确的。