首页 > 解决方案 > 常量表达式中的模板化委托复制构造函数

问题描述

这个问题是由这个引起的

考虑以下代码:

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 拒绝了它(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;两个编译器都会拒绝它

我的问题是:

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

标签: c++templateslanguage-lawyercopy-constructorconstant-expression

解决方案


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

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

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

根据隐式定义的复制构造函数的定义,没有#1任何不同的方式,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)复制构造函数,抑制了隐式定义的复制构造函数,因此委托构造调用模板 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&)不会抑制隐式定义的复制构造函数,并且永远不能优先于隐式定义的复制构造函数,因为它是一个模板并且参数列表是相同的。


推荐阅读