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 的示例)、B
和 C
:
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 B
和 C
的模板构造函数,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
示例。
在下面的程序中 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 的示例)、B
和 C
:
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 B
和 C
的模板构造函数,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
示例。