Class 需要可复制参数的构造函数模板本身不可复制

Class with constructor template requiring copyable argument is not copyable by itself

在下面的程序中 struct A 有一个构造函数模板 A(T) 要求类型 T 是可复制构造的。同时 A 本身必须隐式定义复制构造函数:

#include <type_traits>

struct A {
    template <class T>
    A(T) requires(std::is_copy_constructible_v<T>) {}
};
static_assert(std::is_copy_constructible_v<A>);

最后一个 static_assert(std::is_copy_constructible_v<A>) 在 GCC 和 MSVC 中通过,但 Clang 拒绝了,抱怨:

error: substitution into constraint expression resulted in a non-constant expression
    A(T) requires(std::is_copy_constructible_v<T>) {}
                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/12.0.1/../../../../include/c++/12.0.1/type_traits:971:30: note: while checking constraint satisfaction for template 'A<A>' required here
    : public __bool_constant<__is_constructible(_Tp, _Args...)>
                             ^~~~~~~~~~~~~~~~~~
...

演示:https://gcc.godbolt.org/z/shKe7W1jr

这只是一个 Clang 错误吗?

TLDR

给定以下示例 A(OP 的示例)、BC

struct A {
    template <class T>
    A(T) requires(!std::is_copy_constructible_v<T>) {}

};
static_assert(std::is_copy_constructible_v<A>);  // #OP


struct B {
    template <class T>
    B(const T&) requires(!std::is_copy_constructible_v<T>) {}

};
static_assert(std::is_copy_constructible_v<B>);  // #i

struct C {
    template <class T>
    C(T&&) requires(!std::is_copy_constructible_v<T>) {}

};
static_assert(std::is_copy_constructible_v<C>);  // #ii

然后:

  • A#OP 是 well-formed

    • [rejects-invalid] Clang 拒绝是错误的
    • [accepts-valid] GCC和MVSC接受是正确的
  • B#i 可以说是 ill-formed 由于重载解析期间的递归

    • [rejects-valid] Clang 拒绝是正确的
    • [accepts-invalid] GCC 和 MVSC 可以说是不正确的接受它
  • C#ii 是 well-formed

    • [accepts-valid]Clang、GCC、MVSC正确接受

详情

首先,根据 [class.copy.ctor]/1 and [class.copy.ctor]/2 a template constructor is never a copy or a move constructor, respectively, meaning the rules about under which conditions move/copy constructors and assignment ops are implicitly declared, [class.copy.ctor]/6 and [class.copy.ctor]/8,不受模板构造函数的影响。

/1 A non-template constructor for class X is a copy constructor if its first parameter is of type X&, const X&, volatile X& or const volatile X&, and either there are no other parameters or else all other parameters have default arguments ([dcl.fct.default])

/2 A non-template constructor for class X is a move constructor if its first parameter is of type X&&, const X&&, volatile X&&, or const volatile X&&, and either there are no other parameters or else all other parameters have default arguments ([dcl.fct.default]).

/6 If the class definition does not explicitly declare a copy constructor, a non-explicit one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted ([dcl.fct.def]).

/8 If the definition of a class X does not explicitly declare a move constructor, a non-explicit one will be implicitly declared as defaulted if and only if

  • X does not have a user-declared copy constructor,
  • X does not have a user-declared copy assignment operator,
  • X does not have a user-declared move assignment operator, and
  • X does not have a user-declared destructor.

这意味着 OP 的程序可能不会被拒绝为 ill-formed 因为 class A 不是可复制构造的。这使得程序在重载解析期间由于错误 ill-formed,特别是由 static_assert(std::is_copy_constructible_v<A>); 触发,作为特征的一部分,它将尝试构造一个类型 A,其参数类型为 [=28] =].即使这是在未计算的上下文中完成的,它也会触发重载决议,其中 A 的所有构造函数,包括模板构造函数,都是候选对象。简体:

static_assert(std::is_copy_constructible_v<A>);
// overload res. for A obj("A const& arg")
// 1) candidates:  a) copy ctor
//                 b) move ctor
//                 c) template ctor ?
// 2) viable candidates ?

根据 [over.match.funcs]/7 模板 ctor 是候选者

In each case where a candidate is a function template, candidate function template specializations are generated using template argument deduction ([temp.over], [temp.deduct]) [...]

然而,生成的候选函数模板特化将是(递归构造函数)A(A),并且根据 [class.copy.ctor]/5 永远不会使用模板构造函数来产生这样的特化:

/5 A declaration of a constructor for a class X is ill-formed if its first parameter is of type cv X and either there are no other parameters or else all other parameters have default arguments. A member function template is never instantiated to produce such a constructor signature.

因此,模板构造函数的特化甚至从未进入候选函数集,这意味着我们既不会达到按照通常的重载决议规则拒绝候选的状态,也不会达到由于约束失败而拒绝候选的状态。

因此,由于重载决议只包含隐式生成的复制和移动构造函数,Clang 拒绝 OP 的程序是错误的。


正如@Jarod42 所指出的,一个更有趣的例子是模板构造函数有一个参数 T const&T&&(分别是左值常量引用和 universal/forwarding 引用):

struct B {
    template <class T>
    B(const T&) requires(!std::is_copy_constructible_v<T>) {}

};
static_assert(std::is_copy_constructible_v<B>);  // #i

struct C {
    template <class T>
    C(T&&) requires(!std::is_copy_constructible_v<T>) {}

};
static_assert(std::is_copy_constructible_v<C>);  // #ii

奇怪的是,虽然 GCC 和 MVSC 接受这两种情况,但 Clang 拒绝 #i 并显示与 OP 示例相同的错误消息,但接受 #ii.

对于 classes BC 的模板构造函数,class.copy.ctor]/5 的特殊情况不适用,这意味着 #i#ii将分别进入未评估调用的候选集B obj("B const& arg")C obj("C const& arg")

因此,这些模板构造器将进入寻找 可行 候选人的阶段,此时将根据 [over.match.viable]/3.

检查约束条件

对于B及其B(const T&)构造函数,包括约束在内的候选者是

// T = B
B(const B&) requires requires(std::is_copy_constructible_v<B>)

意味着作为检查特征的一部分(回忆一下静态断言)

std::is_copy_constructible_v<B>

我们运行对相同特征进行约束满足检查,完全解决第一个检查之前,这意味着递归。我一直无法找到明确的措辞来拒绝这样的程序,但按理说重载解析期间的递归应该导致 ill-formed 程序。因此,Clang 拒绝 B 示例可以说是正确的。

对于C及其C(T&&)构造函数,包括约束在内的候选者是

// T = C const&
C(C const&) requires requires(std::is_copy_constructible_v<C const&>)

B 的示例相反,这 不会 导致递归,因为 std::is_copy_constructible_v<C const&> 始终为真(对任何类型都为真 C 是可引用的,例如除 void 或 cv-/ref-qualified 函数类型之外的任何类型)。

因此,可以说所有编译器都可以正确接受 C 示例。