【问题标题】:constexpr array of constexpr objects using move ctorconstexpr 使用 move ctor 的 constexpr 对象数组
【发布时间】:2014-12-04 20:41:42
【问题描述】:

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

class  C {
    public:
        constexpr C(int) { }
        C(const C&) = delete;
        C& operator=(const C&) = delete;
};

int main() {
    constexpr C arr[] = {1, 2};
}

我发现这段代码不起作用,因为它实际上是在尝试使用 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 构造函数必须有空体。 (我在 move ctor 中有代码的原因不值得深入探讨)。

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

注意

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

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}};
}

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

【问题讨论】:

  • GCC 在启用 -fno-elide-constructors 的情况下失败,所以我想它应该也会在启用复制省略的情况下失败,就像 Clang 一样。使用{{1},{2}} 复制列表初始化对象,它不调用任何复制ctor,直接调用constexpr ctor,因此不会出现错误并且两个编译器都是正确的

标签: c++ c++11 c++14 constexpr


【解决方案1】:

那么谁在这里呢?

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 不合格版本的临时版本 目的地类型。临时是prvalue。调用结果 (这是构造函数的临时情况)然后用于 直接初始化,根据上面的规则,对象是 复制初始化的目的地。

在第二个示例中,copy-list-initialization 适用 - and no temporary is introduced

顺便说一句:GCC 4.9 does not compile the above,即使没有提供任何警告标志。

【讨论】:

  • 当我得到这个时使用 g++ 4.9.2 和 c++11
【解决方案2】:

§8.5 [dcl.init]/p17:

初始化器的语义如下。目的地类型是 正在初始化的对象或引用的类型和源 type 是初始化表达式的类型。如果初始化器是 不是单个(可能是括号)表达式,源类型是 未定义。

  • 如果初始化程序是(非括号)braced-init-list,则对象或引用是列表初始化的 (8.5.4)。
  • [...]
  • 如果目标类型是(可能是 cv 限定的)类类型:
    • 如果初始化是直接初始化,或者如果是复制初始化,其中源的 cv 不合格版本 type 与该类的类相同或派生类 目的地,[...]
    • 否则(即,对于剩余的复制初始化情况),可以从源转换的用户定义转换序列 类型为目标类型或(使用转换函数时) 其派生类的枚举如 13.3.1.4 中所述, 最好的一个是通过重载决议(13.3)选择的。如果 转换无法完成或不明确,初始化是 格式不正确。使用初始化程序调用所选函数 表达式作为它的参数;如果函数是构造函数,则调用 初始化 cv 不合格版本的临时版本 目的地类型。临时是prvalue。调用结果 (这是构造函数的临时情况)然后用于 直接初始化,根据上面的规则,对象是 复制初始化的目的地。在某些情况下,一个 允许实施以消除在此固有的复制 通过直接构造中间结果来直接初始化 进入正在初始化的对象;见 12.2、12.8。
  • [...]

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

当一个聚合被初始化列表初始化时,指定 在 8.5.4 中,初始化列表的元素被视为 聚合成员的初始化器,增加下标 或会员订单。每个成员都从 相应的初始化子句。如果 initializer-clause 是 表达式和缩小转换 (8.5.4) 需要转换 表达式,程序格式错误。 [ 注意:如果 initializer-clause 本身就是一个初始化列表,成员是 列表初始化,这将导致递归应用 如果成员是聚合,则在本节中的规则。 —尾注 ]

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

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

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

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

对于constexpr C arr[] = {{1}, {2}};,元素是copy-list-initialized,它不构造临时对象(注意在§8.5.4 [dcl. init.list]/p3).

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-09-23
    • 1970-01-01
    相关资源
    最近更新 更多