值初始化:MSVC 与 clang

Value initialization: MSVC vs clang

本文关键字:clang MSVC 初始化      更新时间:2023-10-16
#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.

如果我在As 构造函数中使用简单的括号来(即A() : m_a() {}),它编译得很好。从 cpp首选项中,我怀疑两者都应该导致相同的结果(即值初始化)。我错过了什么还是其中一个编译器中的错误?

Clang是正确的。

您的困惑来自:

从 cpp首选项中,我怀疑两者都应该导致相同的结果(即值初始化)。

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

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

这意味着当使用大括号初始化列表时,对于聚合类型,首选执行聚合初始化。使用A() : m_a{} {},并且m_a是一个属于聚合类型的数组,则改为执行聚合初始化:

(强调我的)

每个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;复制初始化不会调用explicit构造函数:

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


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

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

然后

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

然后调用S的默认构造函数来初始化m_a的元素。是否explicit对于默认初始化无关紧要。

对于m_a{}

[dcl.init]/17.1 将我们发送到 [dcl.init.list],[dcl.init.list
  • ]/3.4 表示我们根据 [dcl.init.aggr] 对m_a执行聚合初始化。

    初始值设定项的语义如下所示。[...]

    • 如果初始值设定项是(非括号)大括号初始化列表=大括号初始化列表,则对象或引用是列表初始化的。
    • [...]

    类型为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是没有默认构造函数 ([class.ctor]) 或用户提供或删除的默认构造函数的(可能符合 CV 条件的)类类型,则该对象是默认初始化的;
    • [...]
  • 命中 [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] 枚举构造函数,并在初始值设定项()上执行重载解析;

  • 这一次,默认初始化不在复制初始化的上下文中,因此候选函数是"正在初始化的对象类的所有构造函数"。
  • 这一次,显式默认构造函数候选函数,并通过重载解析选择。因此,该程序格式良好。

这显然是标准的格式不正确(问题是,为什么?

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()的语法不是聚合成员初始化,并且不调用复制初始化

如果我理解标准正确 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

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