值初始化:MSVC vs clang

15
#include<cstddef>

template<typename T, std::size_t N>
struct A {
    T m_a[N];
    A() : m_a{} {}
};

struct S {
    explicit S(int i=4) {}
};

int main() {
    A<S, 3> an;
}

以上代码在MSVC(2017)中编译良好,但在clang 3.8.0中失败(clang++ --version && clang++ -std=c++14 -Wall -pedantic main.cpp输出):

clang version 3.8.0 (tags/RELEASE_380/final 263969)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/bin
main.cpp:6:15: error: chosen constructor is explicit in copy-initialization
    A() : m_a{} {}
              ^
main.cpp:14:13: note: in instantiation of member function 'A<S, 3>::A' requested here
    A<S, 3> an;
            ^
main.cpp:10:14: note: constructor declared here
    explicit S(int i=4) {}
             ^
main.cpp:6:15: note: in implicit initialization of array element 0 with omitted initializer
    A() : m_a{} {}
              ^
1 error generated.

clang 5.0也拒绝编译这个代码:

<source>:6:17: error: expected member name or ';' after declaration specifiers
    A() : m_a{} {}
                ^
<source>:6:14: error: expected '('
    A() : m_a{} {}
             ^
2 errors generated.

如果我在A的构造函数中使用简单括号(即A():m_a(){}),它可以编译通过。根据cppreference,我本来以为两者应该是相同的(即值初始化)。我是否遗漏了什么,还是这是编译器中的一个错误?

2
注意 gcc 会产生警告但它可以构建 - Shafik Yaghmour
3
Clang 3.8.0相当陈旧了。您尝试过使用现代的Clang(例如5.0.1)吗? - Jesper Juhl
2
@JesperJuhl 在clang的主干版本上无法编译 :( - Rakete1111
4个回答

10

Clang是正确的。

你的困惑来自于:

根据cppreference,我本来以为两者应该会得到相同的结果(即值初始化)。

实际上它们有不同的效果。请注意该页面中的注释:

在所有情况下,如果使用空括号{}并且T是聚合类型,则执行聚合初始化而不是值初始化。

这意味着,当使用花括号初始化时,对于聚合类型,优先执行聚合初始化。在 A() : m_a{} {} 中,m_a 是一个数组,属于聚合类型,因此将执行聚合初始化

(强调是我的)

每个直接公共基类、(自C++17起)数组元素或非静态类成员,按数组下标/在类定义中出现的顺序,从初始化器列表的相应子句进行复制初始化

如果初始化器子句的数量小于成员数和基数(自C++17起),或者初始化器列表完全为空,则其余成员和基数(自C++14起)将根据常规列表初始化规则进行初始化,即对于非类类型和具有默认构造函数的非聚合类执行值初始化,并对于聚合体执行聚合初始化。

这意味着,剩余的元素,即m_a的所有3个元素,将从空列表中进行复制初始化;对于空列表,将考虑S的默认构造函数,但它被声明为explicit复制初始化不会调用explicit构造函数:

复制列表初始化(显式和非显式构造函数都会被考虑,但只有非显式构造函数可以被调用)


另一方面,A() : m_a() {} 执行值初始化,然后

3)如果T是数组类型,则数组的每个元素都将进行值初始化;

然后进行:

1) 如果T是一个没有默认构造函数或者有用户提供或删除的默认构造函数的类类型,那么对象将进行默认初始化;

然后调用S的默认构造函数来初始化m_a中的元素。对于默认初始化来说,是否为explicit并不重要。


2
顺便提一下,相关的标准部分是[over.match.list]/1。 - NathanOliver

3
对于 m_a{}
  • [dcl.init]/17.1把我们带到[dcl.init.list],而[dcl.init.list]/3.4说我们在m_a上执行聚合初始化,使用[dcl.init.aggr]

    初始化程序的语义如下。[...]

    • 如果初始化程序是一个(非括号)花括号初始化列表或是=花括号初始化列表,则对象或引用被列表初始化。
    • [...]

    类型为T的对象或引用的列表初始化定义如下:

    • [...]
    • 否则,如果T是一个聚合体,则执行聚合初始化。
    • [...]
  • [dcl.init.aggr]/5.2说我们从空初始化列表即{}复制初始化m_a的每个元素。

    对于非联合聚合体,未显式初始化的每个元素的初始化方式如下:

    • [...]
    • 否则,如果该元素不是引用,则从空初始化列表([dcl.init.list])中进行复制初始化。
    • [...]
  • 这使我们回到[dcl.init]/17.1,对每个元素进行初始化,再次将我们带到[dcl.init.list]
  • 这一次我们看到了[dcl.init.list]/3.5,它说该元素是值初始化的。

    类型为T的对象或引用的列表初始化定义如下:

    • [...]
    • 否则,如果初始化程序列表没有元素并且T是一个具有默认构造函数的类类型,则该对象被值初始化。
    • [...]
  • 这使我们到达[dcl.init]/8.1,它说该元素是默认初始化的。

    对于类型T的对象进行值初始化的意思是:

    • 如果T是一个(可能带有cv限定符的)类类型,没有默认构造函数([class.ctor])或者默认构造函数是用户提供的或已删除,则对象被默认初始化;
    • [...]
  • 这使我们到达[dcl.init]/7.1,它说我们按[over.match.ctor]枚举构造函数,并在初始化器()上执行重载决议;

    对于类型T的对象进行默认初始化的意思是:

    • 如果T是一个(可能带有cv限定符的)类类型,则考虑构造函数。适用的构造函数被枚举([over.match.ctor]),并为初始化对象选择最佳构造函数。()。因此选择的构造函数将使用空参数列表调用以初始化对象。
    • [...]
  • [over.match.ctor]说:

    对于直接初始化或不在复制初始化上下文中的默认初始化,候选函数是正在初始化的对象的类的所有构造函数。对于复制初始化,候选函数是该类的所有转换构造函数。

  • 这个默认初始化

    对于 m_a()
    • 我们遇到了[dcl.init]/17.4, 它说明了数组的值初始化。

      初始化器的语义如下。[...]

      • [...]
      • 如果初始化器是(),对象将被值初始化。
      • [...]
    • 这就带我们来到了[dcl.init]/8.3, 它说明每个元素都会被值初始化。

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

      • [...]
      • 如果T是数组类型,则每个元素都会被值初始化;
      • [...]
    • 这又带我们回到了[dcl.init]/8.1,然后到达[dcl.init]/7.1,所以我们再次根据[over.match.ctor]枚举构造函数,并在初始化程序上执行重载解析()

    • 这一次,默认初始化不是在复制初始化的上下文中,因此候选函数是“正在初始化的对象的类的所有构造函数”。
    • 这一次,显式的默认构造函数一个候选函数,并通过重载解析进行选择。因此程序是合法的。

2

这段代码在标准中明确被认为是不合法的(但问题是,为什么呢?):

m_a{}用于对S::m_a进行列表初始化:

[dcl.init.list]/1

列表初始化是指从一个大括号初始化列表初始化对象或引用。这样的初始化器被称为初始化列表,由初始化列表的逗号分隔的初始化子句或者指定初始化子句指定初始化列表被称为初始化列表的元素。初始化列表可能为空。列表初始化可以在直接初始化或者拷贝初始化上下文中出现;在直接初始化上下文中的列表初始化被称为直接列表初始化,在拷贝初始化上下文中的列表初始化被称为拷贝列表初始化

作为一个数组,A<S, 3>::m_a是一个聚合类型([dcl.init.aggr]/1)。

[dcl.init.aggr]/3.3

  1. 当以[dcl.init.list]中指定的初始化器列表对聚合进行初始化时,[...]
    3.3 初始化器列表必须是{},且没有明确定义的元素。

根据上述规则,由于没有明确定义的元素,因此:

[dcl.init.aggr]/5.2

  1. 对于非联合聚合,如果一个元素不是明确定义的,则其初始化如下:[...]
    5.2 如果元素不是引用,则从空初始化列表([dcl.init.list])中进行拷贝初始化

因此,每个A<S, 3>::m_a中的S将进行拷贝初始化

[dcl.init]/17.6.3

  1. 初始化程序的语义如下。被初始化对象或引用的目标类型是该类型,而初始化表达式的源类型是该表达式的类型。 如果初始化程序不是单个(可能带括号)的表达式,则源类型未定义。[...]
    17.6 如果目标类型是(可能带有cv限定符的)类类型:[...]
    17.6.3 否则(即对于其余的复制初始化情况),枚举可以从源类型转换到目标类型的用户定义转换序列(当使用转换函数时,也可以转换为派生类),如[over.match.copy]中所述,并通过重载分辨选择最佳的转换序列。如果无法进行转换或转换不明确,则初始化程序是非法的。

由于S的默认构造函数是显式的,因此它无法从源类型转换为目标类型S)。

另一方面,使用m_a()的语法不是聚合成员初始化,也不会调用复制初始化


2
错误的引号。聚合初始化从{}复制初始化省略的元素,因此使用列表初始化规则。 - T.C.
还是错的。你从来没有触发[dcl.init]/17.6.3。(空初始化列表的“源类型”是什么?) - T.C.
如果初始化程序不是单个(可能带括号的)表达式,则源类型未定义。 - YSC
@T.C. 或许你可以帮我更好地理解。那么,"[dcl.init.aggr]/5.2" 中的 "如果元素不是引用,则从空初始化器列表进行复制初始化" 是什么意思呢? - YSC
显示剩余3条评论

0

如果我正确理解标准,那么clang是正确的。

根据[dcl.init.aggr]/8.5.1:2:

当聚合体通过初始化器列表进行初始化时,如8.5.4所指定的那样,初始化器列表的元素将按照递增的下标或成员顺序作为聚合体成员的初始化器。每个成员都从相应的初始化器子句中进行复制初始化。

在同一条款[dcl.init.aggr]/8.5.1:7中进一步说明:

如果初始化器列表中的初始化器子句少于聚合体中的成员,则未明确初始化的每个成员都应从其大括号或等号初始化程序进行初始化,或者如果没有大括号或等号初始化程序,则从空初始化器列表进行初始化。

根据列表初始化规则[over.match.list]/13.3.1.7:

在复制列表初始化中,如果选择了显式构造函数,则初始化将不合法。


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