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

Declaring defaulted assignment operator as constexpr: which compiler is right?

本文关键字:编译器 赋值运算符 默认 声明 constexpr      更新时间:2023-10-16

>考虑

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.

(现场演示)

哪个编译器是正确的,为什么?

我认为这三个编译器都是错误的。

[dcl.fct.def.default]/3 说:

未定义为已删除的显式默认函数只有在隐式声明为constexpr时才可以声明为constexprconsteval。如果函数在其第一个声明上显式默认,则隐式认为它是constexpr隐式声明是否默认。

复制赋值运算符何时隐式声明为constexpr?[class.copy.assign]/10:

隐式定义的复制/移动赋值运算符是 constexpr,如果

  • X 是文本类型,并且
  • [...]

如果文本类型为,则来自 [basic.types]/10:

如果类型是以下类型,则类型为文本类型:

  • [...]
  • 可能符合 CV 条件的类类型,具有以下所有属性:

    • 它有一个微不足道的析构函数,
    • [...]

A1没有简单的析构函数,因此其隐式复制赋值运算符不是constexpr。因此,复制赋值运算符格式不正确(gcc 和 msvc 错误接受)。

另外两个都很好,拒绝A2是个叮当声。


请注意我引用的[dcl.fct.def.default]的最后一点。如果您显式默认,则实际上不必添加constexpr。在可能的情况下,这将是隐含constexpr的。

C++17 标准规定:

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

10 当 odr 使用 (6.2) 时(例如,当重载解析选择它分配给其类类型的对象时),或者在第一次声明后显式默认时,隐式定义默认且未定义为已删除的类 X 的复制/移动赋值运算符。隐式定义的复制/移动赋值运算符constexprif
(10.1) —X是文字类型,和
(10.2) — 选择复制/移动每个直接基类子对象的赋值运算符是一个constexpr函数,并且
(10.3) — 对于类类型(或其数组)的每个非静态数据成员X,选择复制/移动该成员的赋值运算符是一个constexpr函数。

复制赋值运算符在其中两种情况下满足上述要求。在第一种情况下,由于非平凡析构函数,我们有一个非文字类型。

所以我认为Clang在第二种情况下拒绝代码是错误的。

有一个提交给 Clang 的错误,标题为:默认析构函数阻止在默认的复制/移动运算符上使用 constexpr,该运算符显示与 OP 中的代码相同的症状。

错误报告中的注释指出:

当默认析构函数被注释掉(即不是用户声明的)时,错误将不复存在。

如果在复制赋值运算符之前声明析构函数,问题也会消失。

问题中的代码也是如此。

正如@YSC指出的,这里另一个相关的引用是:[dcl.fct.def.default]/3,其中指出:

未定义为已删除的显式默认函数只有在隐式声明为constexpr时才可以声明为constexprconsteval。如果函数在其第一个声明上显式默认,则隐式认为它是constexpr隐式声明是否默认。