值初始化:MSVC vs clang

时间:2018-02-19 16:43:51

标签: c++ visual-studio initialization clang language-lawyer

#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我怀疑两者都应该相同(即值初始化)。我错过了什么,或者这是其中一个编译器中的错误?

4 个答案:

答案 0 :(得分:10)

Clang是对的。

你的困惑来自:

  

cppreference我怀疑两者都应该相同(即值初始化)。

不,他们有不同的影响。请注意该页面中的注释:

  

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

这意味着当使用braced-init-list初始化时,对于聚合类型,首选聚合初始化。使用A() : m_a{} {}m_a是一个属于aggregate type的数组,然后执行aggregate initialization

(强调我的)

  

每个direct public base, (since C++17)数组元素或非静态类成员,按照类定义中数组下标/外观的顺序,从初始化列表的相应子句中复制初始化

  

如果初始化程序子句的数量少于成员数and bases (since C++17)或初始化程序列表完全为空,则其余成员and bases (since C++17)将按空列表初始化by their default initializers, if provided in the class definition, and otherwise (since C++14),具体如下:通常的列表初始化规则(使用默认构造函数执行非类类型和非聚合类的值初始化,以及聚合的聚合初始化)。

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

  

copy-list-initialization(考虑显式和非显式构造函数,但只能调用非显式构造函数)

另一方面,A() : m_a() {}执行value initialization,然后执行

  

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

然后

  

1)如果T是没有默认构造函数的类类型,或者是用户提供或删除的默认构造函数,则该对象是默认初始化的;

然后调用S的默认构造函数来初始化m_a的元素。是explicit是否与default initialization无关。

答案 1 :(得分:3)

m_a{}

  • [dcl.init]/17.1将我们发送至[dcl.init.list][dcl.init.list]/3.4表示我们按m_a[dcl.init.aggr]执行汇总初始化。

      

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

         
        
    • 如果初始化程序是(非括号内的) braced-init-list = braced-init-list ,则对象或引用是列表初始化。
    •   
    • [...]
    •   
               

    类型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,它表示该元素是默认初始化的。

      

    value-initialize 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,它说每个元素都是值初始化的。

      

    value-initialize T类型的对象意味着:

         
        
    • [...]
    •   
    • 如果T是数组类型,则每个元素都是值初始化的;
    •   
    • [...]
    •   
  • 这再次将我们带到[dcl.init]/8.1,然后再转到[dcl.init]/7.1,因此我们再次按[over.match.ctor]枚举构造函数并对初始化程序()执行重载解析

  • 这次,在复制初始化的上下文中,默认初始化不是,因此候选函数是“正在初始化的对象类的所有构造函数”。
  • 这次,显式默认构造函数候选者,并通过重载决策选择。所以该计划结构良好。

答案 2 :(得分:2)

这标准明确不正确(问题是,为什么?):

m_a{}列表初始化S::m_a

  

[dcl.init.list]/1

     

列表初始化是从 braced-init-list 初始化对象或引用。   这样的初始化程序称为初始化程序列表,初始化程序列表指定的初始化程序子句的逗号分隔初始化程序子句 specified-initializer-list 被称为初始化列表的元素。初始化列表可以为空。列表初始化可以在直接初始化复制初始化上下文中进行; 直接初始化上下文中的 list-initialization 称为 direct-list-initialization list-initialization copy-initialization 上下文称为 copy-list-initialization

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

  

[dcl.init.aggr]/3.3

     
      
  1. 当聚合由[dcl.init.list]中指定的初始化列表初始化时,[...]
      3.3初始化列表必须为{},并且没有明确初始化的元素。
  2.   

以下,因为没有明确初始化的元素

  

[dcl.init.aggr]/5.2

     
      
  1. 对于非联合聚合,每个不是显式初始化元素的元素都初始化如下:[...]
      5.2如果元素不是引用,则元素从空的初始化列表([dcl.init.list]复制初始化
  2.   

S的每个A<S, 3>::m_a,然后复制初始化

  

[dcl.init]/17.6.3

     
      
  1. 初始化器的语义如下。   目标类型是要初始化的对象或引用的类型,源类型是初始化表达式的类型。   如果初始化程序不是单个(可能带括号的)表达式,则不定义源类型。 [...]
      17.6如果目的地类型是(可能是cv限定的)类类型:[...]
      17.6.3否则(即,对于剩余的复制初始化情况),用户定义的转换序列可以将从源类型转换为目标类型或(当转换函数用于其派生类,如[over.match.copy]中所述列举,并且通过重载决策选择最佳的一个。   如果转换无法完成或模糊不清,则初始化格式不正确。
  2.   

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

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

答案 3 :(得分:0)

如果我理解标准正确的铿锵是正确的。

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

  

按指定的初始化列表初始化聚合时   在8.5.4中,初始化列表的元素被视为   增加下标的聚合成员的初始化程序   或会员订单。每个成员都是从中复制初始化的   相应的初始化条款。

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

  

如果列表中的初始化子句数少于   聚合中的成员,然后每个成员未明确初始化   应从其支撑或等于初始化器初始化,或者如果有的话   从空的初始化列表

中没有大括号或等于初始化器

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

  

在copy-list-initialization中,如果选择了显式构造函数,则   初始化是不正确的。