提问人:Fedor 提问时间:1/27/2022 更新时间:1/28/2022 访问量:473
具有需要可复制参数的构造函数模板的类本身不可复制
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>);
以及 GCC 和 MSVC 的最后通过,但 Clang 拒绝了它,抱怨道:static_assert(std::is_copy_constructible_v<A>)
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 错误吗?
答:
顶级域名
给出以下示例(OP 的示例),以及:A
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
- [拒绝 - 无效]Clang拒绝它是错误的
- [接受有效]GCC 和 MVSC 接受它是正确的
B
with 可以说是格式不正确的,因为在重载解决期间递归#i
- [拒绝有效]Clang 拒绝它是正确的
- [接受无效]GCC 和 MVSC 接受它可以说是不正确的
C
与是良好的格式#ii
- [接受有效]Clang、GCC 和 MVSC 接受它是正确的
详
首先,根据 [class.copy.ctor]/1 和 [class.copy.ctor]/2,模板构造函数分别从来都不是副本或移动构造函数,这意味着关于在哪些条件下隐式声明移动/复制构造函数和赋值操作的规则,[class.copy.ctor]/6 和 [class.copy.ctor ]/8,不受模板构造函数的影响。
/1 类 X 的非模板构造函数是复制构造函数,如果 它的第一个参数是 X&、常量 X&、易失性 X& 或常量 易失性 X&,要么没有其他参数,要么全部参数 其他参数具有默认参数 ([dcl.fct.default])
/2 类 X 的非模板构造函数是移动构造函数,如果 它的第一个参数类型为 X&&、常量 X&&、易失性 X&& 或常量 易失性 X&&,要么没有其他参数,要么全部参数 其他参数具有默认参数 ([dcl.fct.default])。
/6 如果类定义未显式声明副本 构造函数,则隐式声明一个非显式构造函数。如果 类定义声明 Move 构造函数或 Move 赋值 运算符,隐式声明的复制构造函数定义为 删除;否则,它被定义为默认值 ([dcl.fct.def])。
/8 如果类 X 的定义没有显式声明移动 构造函数,一个非显式的构造函数将被隐式声明为 违约 当且仅当
- X 没有用户声明的复制构造函数,
- X 没有用户声明的复制分配运算符,
- X 没有用户声明的移动赋值运算符,并且
- X 没有用户声明的析构函数。
这意味着 OP 的程序可能不会因为格式错误而被拒绝,因为该类不可复制构造。这使得程序在重载解析过程中由于错误而格式不正确,特别是由 触发,作为特征的一部分,它将尝试构造具有 type 参数的类型。即使这是在未计算的上下文中完成的,它也会触发重载解析,其中 的所有构造函数(包括模板构造函数)都是候选的。简化:A
static_assert(std::is_copy_constructible_v<A>);
A
A const&
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 是候选者
在候选函数是函数模板的每种情况下,都会使用模板参数推导([temp.over]、[temp.deduct])生成候选函数模板专用化 [...]
但是,生成的候选函数模板专用化将是(递归构造函数),并且根据 [class.copy.ctor]/5,永远不会使用模板构造函数生成这样的专用化:A(A)
/5 类 X 的构造函数声明格式不正确,如果其 第一个参数是 cv X 类型,没有其他参数 参数,否则所有其他参数都具有默认参数。一个 成员函数模板永远不会实例化以生成这样的 构造函数签名。
因此,模板构造函数的专用化甚至永远不会进入候选函数集,这意味着我们永远不会达到按照通常的重载解析规则拒绝候选函数的状态,也不会达到由于约束失败而拒绝候选函数的状态。
因此,由于重载解析仅包含隐式生成的复制和移动构造函数,因此 Clang 拒绝 OP 的程序是错误的。
正如 @Jarod42 所指出的,一个更有趣的例子是当模板构造函数具有参数 or (分别为左值常量引用和通用/转发引用)时:T const&
T&&
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 拒绝并显示与 OP 示例相同的错误消息,但接受 .#i
#ii
对于类 和 的模板构造函数,class.copy.ctor]/5 的特殊情况不适用,这意味着两者都专用化,并将分别进入未计算调用的候选集和 。B
C
#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>
在完全解决第一个检查(即递归)之前,我们会遇到对同一特征的约束满足度检查。我无法找到拒绝此类程序的明确措辞,但按理说,重载解析期间的递归应该会导致格式错误的程序。因此,Clang 拒绝这个例子可以说是正确的。B
对于 和它的构造函数,候选者(包括约束)是C
C(T&&)
// T = C const&
C(C const&) requires requires(std::is_copy_constructible_v<C const&>)
与示例相反,这不会导致递归,因为总是 true(对于任何可引用的类型,例如 any but 或 cv-/ref-qualified 函数类型)。B
std::is_copy_constructible_v<C const&>
C
void
因此,所有编译器都可以说接受这个例子是正确的。C
评论
const T&
A&
T&&
std::is_copy_constructible
A const&
T const&
const T&
T
T const&
T&&
std::is_copy_constructible_v<T>
A
评论
const T&
T&
T&&