我有一个带有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构造函数必须有空体。 (我在移动代码中使用代码的原因并不值得进入)。
那么谁在这里?我的倾向是,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}};
}
我主要担心的是上面哪个编译器是正确的。
答案 0 :(得分:3)
那么谁在这里?
Clang拒绝代码是正确的。 [expr.const] / 2:
条件表达式
e
是核心常量表达式,除非e
的评估,遵循抽象机器的规则 (1.9),将评估以下表达式之一:
- 为文字类,
constexpr
函数或隐式调用调用constexpr
构造函数以外的函数 一个简单的析构函数(12.4)
显然,你的移动构造函数不是constexpr
构造函数 - [dcl.constexpr] / 2
类似地,构造函数声明中使用的
constexpr
说明符 声明构造函数是constexpr
构造函数。
constexpr
对象的初始值设定项的要求在[dcl.constexpr] / 9中:
[...]初始化程序中出现的每个完整表达式都应为a 不断表达。 [注意:每次隐式转换已加入 转换初始化表达式和使用的每个构造函数调用 初始化是这种完整表达的一部分。 - 结束记录 ]
最后,移动构造函数由数组元素的复制初始化调用,并带有相应的初始化子句 - [dcl.init]:
否则(即,对于剩余的复制初始化情况), 用户定义的转换序列,可以从源转换 键入目标类型或(使用转换函数时) 按照13.3.1.4的描述列举其派生类别, 并且通过重载决策(13.3)选择最好的一个。如果 转换不能完成或模糊,初始化是 病态的。使用初始化程序调用所选的函数 表达作为其论点; 如果函数是构造函数,则 call初始化一个临时的cv-nonqualified版本的 目的地类型。临时是一个prvalue。通话的结果 (然后用于构造函数的临时情况) 根据上面的规则,直接初始化对象 复制初始化的目的地。
在第二个示例中, copy-list-initialization 适用 - and no temporary is introduced。
顺便说一下:GCC 4.9 does not compile the above,即使没有提供任何警告标志。
答案 1 :(得分:3)
§8.5[dcl.init] / p17:
初始化器的语义如下。目的地类型是 正在初始化的对象或引用的类型以及源 type是初始化表达式的类型。如果初始化程序是 不是单个(可能带括号的)表达式,源类型是 没有定义。
- 如果初始化程序是(非括号内的) braced-init-list ,则对象或引用是列表初始化的(8.5.4)。
- [...]
- 如果目标类型是(可能是cv限定的)类类型:
- 如果初始化是直接初始化,或者是复制初始化,那么cv-nonqualified版本的源 type与类的类相同,或者是类的派生类 目的地,[...]
- 否则(即,对于剩余的复制初始化情况),可以从源转换的用户定义的转换序列 键入目标类型或(使用转换函数时) 按照13.3.1.4的描述列举其派生类别, 并且通过重载决策(13.3)选择最好的一个。如果 转换不能完成或模糊,初始化是 病态的。使用初始化程序调用所选的函数 表达作为其论点;如果函数是构造函数,则调用 初始化一个临时的cv-nonqualified版本的 目的地类型。临时是一个prvalue。通话的结果 (然后用于构造函数的临时情况) 根据上面的规则,直接初始化对象 复制初始化的目的地。在某些情况下, 允许实现消除此中固有的复制 通过直接构造中间结果直接初始化 进入被初始化的对象;见12.2,12.8。
- [...]
§8.5.1[dcl.init.aggr] / p2:
按指定的初始化列表初始化聚合时 在8.5.4中,初始化列表的元素被视为 增加下标的聚合成员的初始化程序 或会员订单。每个成员都是从中复制初始化的 对应的初始化子句。如果 initializer-clause 是 转换需要表达式和缩小转换(8.5.4) 表达方式,该程序是不正确的。 [注意:如果是 initializer-clause 本身就是一个初始化列表,成员是 list-initialized,这将导致递归应用 如果成员是聚合,则本节中的规则。 - 结束记录]
§8.5.4[dcl.init.list] / p3:
对象或类型T的引用的列表初始化定义为 如下:
- 如果T是聚合,则执行聚合初始化(8.5.1)。
- [...]
- 否则,如果T是类类型,则考虑构造函数。枚举适用的构造函数,并选择最佳构造函数 通过重载决议(13.3,13.3.1.7)。如果缩小 转换(见下文)是转换任何参数所必需的, 该计划格式不正确。
- [...]
对于constexpr C arr[] = {1, 2};
,聚合初始化从相应的初始化子句中复制初始化每个元素,即1
和2
。如§8.5[dcl.init] / p17中所述,这构造了一个临时的C
,然后从临时中直接初始化数组元素,这需要一个可访问的副本或移动构造函数。 (可以省略复制/移动,但构造函数仍然可用。)
对于constexpr C arr[] = {{1}, {2}};
,元素是 copy-list-initialized ,而不是构造临时值(请注意,在§8.5.4中没有提到临时构造[ dcl.init.list] / P3)。