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

Templated delegating copy constructor in constant expressions

这个问题的动机是

考虑以下代码:

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();
}

GCC 成功编译此代码,但 Clang 拒绝它 (Example on 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;
  ^

请注意,如果我们删除#2、#3 或#4 中的任何一个,两个编译器都会接受此代码。如果我们将#1 替换为 int b = 0;both compilers reject it.

我的问题是:

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

由于您的两个用户定义的构造函数都是模板,they are not 复制(或移动)构造函数。所以编译器隐式声明了一个复制构造函数,并将其定义为默认值。

第 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);
}

Rejected Clang 和 MSVC,被 gcc 接受;取消注释 #1 让所有三个接受。

根据 Clang 11 的 the implicitly-defined copy constructor there is no way that #1 is any different to constexpr A(A const&) = default; so gcc is correct. Note also that if we give B a user-defined constexpr copy constructor Clang and MSVC again accept, so the issue appears to be that these compilers are unable to track the constexpr copy constructibility of recursively empty implicitly copyable classes. Filed bugs for MSVC and Clang (fixed 定义。

第 2 部分:

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

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

删除 #3S 一个用户定义的(非 const)复制构造函数,抑制隐式定义的复制构造函数,因此委托构造调用模板 const 构造函数(记住, 不是复制构造函数)。

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

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