使用move ctor的constexpr对象的constexpr数组

时间:2014-12-04 20:41:43

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

我有一个带有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}};
}

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

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};,聚合初始化从相应的初始化子句中复制初始化每个元素,即12。如§8.5[dcl.init] / p17中所述,这构造了一个临时的C,然后从临时中直接初始化数组元素,这需要一个可访问的副本或移动构造函数。 (可以省略复制/移动,但构造函数仍然可用。)

对于constexpr C arr[] = {{1}, {2}};,元素是 copy-list-initialized ,而不是构造临时值(请注意,在§8.5.4中没有提到临时构造[ dcl.init.list] / P3)。