【问题标题】:Value initialization: MSVC vs clang值初始化:MSVC vs clang
【发布时间】:2018-07-29 22:43:33
【问题描述】:
#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 &amp;&amp; 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() {}),它编译得很好。从cppreference 我会怀疑两者都应该导致相同的结果(即值初始化)。我是否遗漏了什么或者这是其中一个编译器中的错误?

【问题讨论】:

  • Clang 3.8.0 相当老了。您是否尝试过使用现代 clang(如 5.0.1)?
  • @JesperJuhl 它不能在 clang 主干上编译 :(

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


【解决方案1】:

Clang 是正确的。

你的困惑来自:

来自cppreference,我怀疑两者都应该导致相同的结果(即值初始化)。

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

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

这意味着当使用花括号初始化列表进行初始化时,对于聚合类型,首选执行聚合初始化。用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 的默认构造函数,但它被声明为explicitcopy-initialization 不会调用 explicit 构造函数:

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


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

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

然后

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

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

【讨论】:

  • FWIW 相关标准部分是 [over.match.list]/1
【解决方案2】:

对于m_a{}

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

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

    • 如果初始值设定项是(非括号)braced-init-list=braced-init-list,则对象或引用是 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,它表示该元素是默认初始化的。

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

    • 如果 T 是一个(可能是 cv 限定的)类类型,没有默认构造函数 ([class.ctor]) 或用户提供或删除的默认构造函数,则该对象是默认初始化的;
    • [...]
  • 命中[dcl.init]/7.1,表示我们根据[over.match.ctor] 枚举构造函数,并对初始化器() 执行重载决议;

    默认初始化T 类型的对象意味着:

    • 如果 T 是(可能是 cv 限定的)类类型,则考虑构造函数。枚举适用的构造函数 ([over.match.ctor]),initializer () 的最佳选择是 通过重载决议选择。因此选择的构造函数是 使用空参数列表调用以初始化对象。
    • [...]
  • 和 [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] 枚举构造函数,并对初始化器() 执行重载决议;

  • 这一次,默认初始化不在复制初始化的上下文中,所以候选函数是“被初始化对象的类的所有构造函数”。
  • 这一次,显式默认构造函数一个候选者,并由重载决议选择。所以程序是格式良好的。

【讨论】:

    【解决方案3】:

    这显然是标准错误的(但问题是,为什么?):

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

    [dcl.init.list]/1

    List-initialization 是从braced-init-list 初始化对象或引用。 这样的初始化器称为初始化器列表,以逗号分隔的 initializer-clausesinitializer-listdesignated-initializer-clauses designated-initializer-list 被称为初始化列表的元素。初始化列表可能为空。列表初始化可以发生在 direct-initializationcopy-initialization 上下文中; list-initializationdirect-initialization 上下文中称为 direct-list-initializationlist-initializationcopy-initialization 上下文称为copy-list-initialization

    作为一个数组,A&lt;S, 3&gt;::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&lt;S, 3&gt;::m_a 中的每个 S 都被复制初始化

    [dcl.init]/17.6.3

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

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

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

    【讨论】:

    【解决方案4】:

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

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

    【讨论】:

      猜你喜欢
      • 2015-01-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-04
      • 1970-01-01
      • 1970-01-01
      • 2021-11-02
      • 2014-11-29
      相关资源
      最近更新 更多