常量表达式中的模板化委托复制构造函数

xsk*_*xzr 6 c++ templates copy-constructor language-lawyer constant-expression

这个问题的动机是这个

考虑以下代码:

struct B {};

struct S {
    B b; // #1

    S() = default;

    template <typename ...dummy> // #2
    constexpr S(const S&) {}

    template <typename ...dummy> // #3
    constexpr S(S &other) 
        : S(const_cast<const S&>(other)) // #4
    {}
};

S s;
constexpr S f() {return s;}

int main() {
    constexpr auto x = f();
}
Run Code Online (Sandbox Code Playgroud)

GCC 成功编译了这段代码,但 Clang 拒绝了它(Godbolt.org 上的示例)。Clang 产生的错误信息是

<source>:21:20: error: constexpr variable 'x' must be initialized by a constant expression
    constexpr auto x = f();
                   ^   ~~~
<source>:13:11: note: read of non-constexpr variable 's' is not allowed in a constant expression
        : S(const_cast<const S&>(other)) 
          ^
<source>:13:11: note: in call to 'S(s)'
<source>:18:25: note: in call to 'S(s)'
constexpr S f() {return s;}
                        ^
<source>:21:24: note: in call to 'f()'
    constexpr auto x = f();
                       ^
<source>:17:3: note: declared here
S s;
  ^
Run Code Online (Sandbox Code Playgroud)

请注意,如果我们删除 #2、#3 或 #4 中的任何一个,两个编译器都会接受此代码。如果我们用 替换#1 int b = 0;两个编译器都会拒绝它

我的问题是:

  1. 根据当前标准,哪个编译器是正确的?
  2. 如果 GCC 是正确的,为什么将 #1 替换为int b = 0;使此代码格式错误?如果 Clang 是正确的,为什么删除 #2、#3 或 #4 中的任何一个会使此代码格式良好?

eca*_*mur 3

由于两个用户定义的构造函数都是模板,因此它们不是复制(或移动)构造函数。因此编译器隐式声明一个复制构造函数,并将其定义为默认构造函数。

因此,第 1 部分可归结为以下独特的程序:

struct A {
    struct B {} b;
    constexpr A() {};
    // constexpr A(A const& a) : b{a.b} {}    // #1
};
int main() {
    auto a = A{};
    constexpr int i = (A{a}, 0);
}
Run Code Online (Sandbox Code Playgroud)

被Clang和MSVC拒绝,被gcc接受;取消注释#1以使三个人都接受。

根据隐式定义的复制构造函数的定义,这#1与 gcc 没有任何不同,constexpr A(A const&) = default;因此 gcc 是正确的。另请注意,如果我们给B用户定义的 constexpr 复制构造函数 Clang 和 MSVC 再次接受,那么问题似乎是这些编译器无法跟踪递归空隐式可复制类的 constexpr 复制构造性。提交了MSVCClang的错误(已修复Clang 11)。

第2部分:

删除#1意味着您正在复制(执行左值到右值转换)s.b类型 的对象int,该对象的生命周期开始于 constexpr 上下文之外。

删除#2会给出S用户定义的constexpr复制构造函数,然后将其委托给 at #4

删除#3会给出S用户定义的(非常量)复制构造函数,从而抑制隐式定义的复制构造函数,因此委托构造会调用模板 const 构造函数(请记住,它不是复制构造函数)。

删除#4意味着带有参数的构造函数模板S& other不再调用隐式定义的复制构造函数,因此b默认初始化,Clang 可以在 constexpr 上下文中执行此操作。请注意,复制构造函数仍然隐式声明并定义为默认值,只是template<class...> S::S(S& other)重载解析首选您的构造函数。

认识到抑制隐式定义的复制构造函数和提供首选重载之间的区别非常重要。template<class...> S::S(S&)不抑制隐式定义的复制构造函数,但对于非常量左值参数来说它是首选,假设隐式定义的复制构造函数具有 argument S const&。另一方面,template<class...> S::S(S const&)不会抑制隐式定义的复制构造函数,并且永远不会优于隐式定义的复制构造函数,因为它是模板并且参数列表相同。