将默认赋值运算符声明为constexpr:哪个编译器是正确的?

35
考虑
struct A1 {
    constexpr A1& operator=(const A1&) = default;
    ~A1() {}
};
struct A2 {
    constexpr A2& operator=(const A2&) = default;
    ~A2() = default;
};
struct A3 {
    ~A3() = default;
    constexpr A3& operator=(const A3&) = default;
};

GCC和MSVC都接受这三个结构体。Clang拒绝A1A2(但接受A3),并显示以下错误信息:

<source>:2:5: error: defaulted definition of copy assignment operator is not constexpr
    constexpr A1& operator=(const A1&) = default;
    ^
<source>:6:5: error: defaulted definition of copy assignment operator is not constexpr
    constexpr A2& operator=(const A2&) = default;
    ^
2 errors generated.
(演示)
哪个编译器是正确的,为什么?

好问题。以下只是猜测... 我们知道函数中的“constexpr”并不意味着“const”。它意味着该函数是否可以在编译时计算。默默创建的复制赋值运算符没有前缀“constexpr”。这意味着你拥有的“constexpr”是默默创建的那个的重载。然而,这个重载不能被默认,这就解释了错误。请查看以下3个代码示例:1) (clang) https://rextester.com/WLGFD87794, 2) (gcc) https://rextester.com/RMWQ86797, 3) (vc++) https://rextester.com/MXIHQ50551。 - Constantinos Glynos
2个回答

26
我认为这三个编译器都是错误的。 [dcl.fct.def.default]/3 表示:
显式默认的函数只有在被隐式声明为constexpr时,才可被声明为constexprconsteval,并且未定义为delete。如果一个函数在第一次声明时显式地被默认,那么如果隐式声明会被默认为constexpr,它就会被隐式地认为是constexpr
何时会隐式声明复制赋值运算符为constexpr[class.copy.assign]/10
由此可见,如果
  • X是字面类型,且
  • [...]
  • 则隐含定义的复制/移动赋值运算符是constexpr
    字面类型来自[basic.types]/10
    类型是字面类型的条件是:
  • [...]
  • 一个可能带cv限定符的类类型,具有以下所有属性:

    • 它有一个平凡的析构函数,
    • [...]
  • 所以A1没有一个平凡的析构函数,因此它的隐式复制赋值运算符不是constexpr。因此,该复制赋值运算符是不合法的(gcc和msvc错误接受)。

    另外两个没问题,拒绝A2是clang的一个bug。


    请注意我引用的[dcl.fct.def.default]的最后一部分。如果您明确地使用默认值,实际上不需要添加constexpr。在可能的情况下,它将隐式constexpr

    2
    如果不是constexpr,添加constexpr应该会给你一个错误,这可能非常有用。 - Yakk - Adam Nevraumont

    8
    C++17标准规定:

    15.8.2 复制/移动赋值运算符 [class.copy.assign]
    ...

    10 类X的复制/移动赋值运算符如果是默认的且未被定义为删除,则在其被odr-used(例如,当它被重载分辨率选择以分配给其类类型的对象时)或在其第一次声明后显式地默认时隐式定义。如果隐式定义的复制/移动赋值运算符是constexpr,则:
    (10.1) — X是一个字面量类型,并且
    (10.2) — 被选中以复制/移动每个直接基类子对象的赋值运算符是一个constexpr函数,并且
    (10.3) — 对于X的每个非静态数据成员,如果它是类类型(或其数组),则被选中以复制/移动该成员的赋值运算符是一个constexpr函数。

    复制赋值运算符在两种情况下满足上述要求。在第一种情况下,由于非平凡的析构函数,我们有一个非文字类型。
    所以我认为Clang在拒绝第二种情况的代码时是错误的。
    有一个与Clang相关的错误报告,标题为:默认析构函数防止对默认复制/移动运算符使用constexpr,它展示了与OP中代码相同的症状。
    错误报告中的评论如下:
    当注释掉默认析构函数(即未声明用户)时,错误就会消失。

    如果在复制赋值运算符之前声明析构函数,则问题也会消失。
    这对问题中的代码也是正确的。
    正如@YSC指出的那样,这里还有另一个相关的引用:[dcl.fct.def.default]/3,其中指出:
    如果一个明确默认的函数没有被定义为删除,那么只有当它会被隐式声明为constexpr时,才能将其声明为constexpr或consteval。如果一个函数在其第一次声明上明确默认,则如果隐式声明将会是constexpr,则隐含地被认为是constexpr。

    相关内容:http://eel.is/c++draft/dcl.fct.def.default#3 和 http://eel.is/c++draft/class.copy.assign#10(已引用) - YSC
    我可以翻译一下吗:改变析构函数和复制赋值函数的定义顺序(就像OP对struct A2struct A3所做的那样)不应该对编译代码没有错误产生任何影响,对吗? - Scheff's Cat

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接