使用移动 CTOR 的 constexpr 对象的 constexpr 数组

constexpr array of constexpr objects using move ctor

本文关键字:constexpr 数组 对象 CTOR 移动      更新时间:2023-10-16

我有一个带有constexpr值构造函数的类,但没有复制或移动 ctor

class  C {
    public:
        constexpr C(int) { }
        C(const C&) = delete;
        C& operator=(const C&) = delete;
};
int main() {
    constexpr C arr[] = {1, 2};
}

我发现这段代码不起作用,因为它实际上试图使用 move 构造函数进行C而不是值构造函数来就地构造。 一个问题是我希望这个对象是不可移动的(出于测试目的(,但我想"好吧,好吧,我会添加一个移动构造函数。

class  C {
    public:
        constexpr C(int) { }
        C(const C&) = delete;
        C& operator=(const C&) = delete;
        C& operator=(C&&) = delete;
        C(C&&) { /*something*/ } // added, assume this must be non trivial
};

好吧,现在它使用移动构造函数,并且在 gcc 下一切正常,但是当我使用 clang 时,它会抱怨,因为移动构造函数没有标记为constexpr

error: constexpr variable 'arr' must be initialized by a constant expression
    constexpr C arr[] = {1, 2};

如果我标记移动构造函数constexpr它在 gcc 和 clang 下工作,但问题是我希望在移动构造函数中拥有代码(如果它运行的话(,并且 constexpr 构造函数必须有空体。 (我在移动 ctor 中有代码的原因不值得深入探讨(。

那么谁就在这里?我的倾向是,拒绝代码是正确的。

注意

它确实使用初始值设定项列表和不可复制的不可移动对象进行编译,如下所示:

class  C {
    public:
        constexpr C(int) { }
        C(const C&) = delete;
        C& operator=(const C&) = delete;
        C& operator=(C&&) = delete;
        C(C&&) = delete;
};
int main() {
    constexpr C arr[] = {{1}, {2}};
}

我主要关心的是上面的哪个编译器是正确的。

那么谁就在这里?

Clang拒绝代码是正确的。[expr.const]/2:

条件表达式e是核心常量表达式,除非 e的评估,遵循抽象机的规则 (1.9(,将计算以下表达式之一:

  • 对文本类、constexpr函数或隐式调用的 constexpr 构造函数以外的函数的调用 一个微不足道的析构函数 (12.4(

显然,您的移动构造函数不是constexpr构造函数 - [dcl.constexpr]/2

同样,构造函数声明中使用的constexpr说明符 将该构造函数声明为constexpr构造函数。

constexpr对象的初始值设定项的要求在 [dcl.constexpr]/9 中:

[...] 出现在其初始值设定项中的每个完整表达式都应是 常量表达式。[ 注意每个隐式转换都包含在 转换初始值设定项表达式和所使用的每个构造函数调用 因为初始化是这种完整表达式的一部分。— 尾注 ]

最后,移动构造函数通过使用相应的初始值设定项子句 - [dcl.init] 对数组元素进行复制初始化来调用:

否则(即,对于其余的副本初始化情况(, 可从源转换的用户定义的转换序列 键入为目标类型或(使用转换函数时( 如13.3.1.4中所述枚举其派生类, 最好的一个是通过过载分辨率(13.3(选择的。如果 转换无法完成或模棱两可,初始化为 格式不正确。所选函数使用初始值设定项调用 表达作为其参数;如果函数是构造函数,则 调用初始化 CV 非限定版本的临时版本 目标类型。临时是原则。调用的结果 (这是构造函数情况的临时(然后用于 根据上述规则,直接初始化对象 复制初始化的目标。

在第二个示例中,复制列表初始化适用 - 并且不引入临时。

顺便说一句:GCC 4.9 不会编译上述内容,即使没有提供任何警告标志。

§8.5 [dcl.init]/p17:

初始值设定项的语义如下所示。目标类型为 正在初始化的对象或引用的类型以及源 type 是初始值设定项表达式的类型。如果初始值设定项是 不是单个(可能是括号(表达式,源类型是 未定义。

  • 如果初始值设定项是(非括号(大括号初始化列表,则对象或引用是列表初始化的 (8.5.4(。
  • [...]
  • 如果目标类型是(可能符合 cv 条件的(类类型:
    • 如果初始化是直接初始化,或者如果是复制初始化,其中源的 cv 非限定版本 类型与 目的地, [...]
    • 否则(即,对于剩余的复制初始化情况(,可以从源转换的用户定义的转换序列 键入为目标类型或(使用转换函数时( 如13.3.1.4中所述枚举其派生类, 最好的一个是通过过载分辨率(13.3(选择的。如果 转换无法完成或模棱两可,初始化为 格式不正确。所选函数使用初始值设定项调用 表达作为其参数;如果函数是构造函数,则调用 初始化 CV 非限定版本的临时版本 目标类型。临时是原则。调用的结果 (这是构造函数情况的临时(然后用于 根据上述规则,直接初始化对象 复制初始化的目标。在某些情况下,一个 允许实施以消除此固有的复制 通过直接构造中间结果进行直接初始化 到正在初始化的对象中;参见 12.2、12.8。
  • [...]

§8.5.1 [dcl.init.aggr]/p2:

当聚合由初始值设定项列表初始化时,如指定 在 8.5.4 中,初始值设定项列表的元素取为 聚合成员的初始值设定项,下标递增 或会员订单。每个成员都从 相应的初始值设定项子句。如果初始值设定项子句是 表达式和缩小转换 (8.5.4( 是转换所必需的 表达式,程序格式不正确。[ 注意:如果 初始值设定项子句本身就是一个初始值设定项列表,成员是 列表初始化,这将导致递归应用 如果成员是聚合,则在此部分中的规则。—尾注 ]

§8.5.4 [dcl.init.list]/p3:

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

  • 如果 T 是聚合,则执行聚合初始化 (8.5.1(。
  • [...]
  • 否则,如果 T 是类类型,则考虑构造函数。枚举适用的构造函数并选择最佳构造函数 通过重载解析(13.3、13.3.1.7(。如果变窄 转换(见下文(是转换任何参数所必需的, 程序格式不正确。
  • [...]

对于constexpr C arr[] = {1, 2};,聚合初始化复制初始化每个元素从相应的初始值设定项子句,即12。如 §8.5 [dcl.init]/p17 中所述,这将构造一个临时C,然后从临时元素直接初始化数组元素,这需要一个可访问的复制或移动构造函数。(可以省略复制/移动,但构造函数必须仍然可用。

对于constexpr C arr[] = {{1}, {2}};,元素是复制列表初始化的,它不构造临时的(注意在§8.5.4 [dcl.init.list]/p3中没有提到临时构造(。