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

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

提问人:Fedor 提问时间:1/27/2022 更新时间:1/28/2022 访问量:473

问:

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

#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 错误吗?

C++ 模板 语言律师 C++20 复制构造函数

评论

0赞 Passer By 1/27/2022
是的,它是。甚至不允许模板实例化为复制构造函数。
1赞 Jarod42 1/27/2022
@PasserBy:在被拒绝之前,不应该实例化它吗?由于类仍然不完整,因此无法知道该子句......
0赞 Passer By 1/27/2022
@Jarod42 请参见 [class.copy.ctor]。
1赞 Jarod42 1/27/2022
@PasserBy:这就处理了OP的情况。(但问题仍然存在 (Demo), , (但不是 (Demo))))...const T&T&T&&
1赞 Jarod42 1/27/2022
@dfrib:我没有说隐式生成被抑制,但应该考虑所有过载。([class.copy.ctor]/5 确实删除了 OP 代码的问题,但没有删除其变体)。

答:

2赞 dfrib 1/27/2022 #1

顶级域名

给出以下示例(OP 的示例),以及:ABC

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 接受它是正确的
  • Bwith 可以说是格式不正确的,因为在重载解决期间递归#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 参数的类型。即使这是在未计算的上下文中完成的,它也会触发重载解析,其中 的所有构造函数(包括模板构造函数)都是候选的。简化:Astatic_assert(std::is_copy_constructible_v<A>);AA 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 的特殊情况不适用,这意味着两者都专用化,并将分别进入未计算调用的候选集和 。BC#i#iiB obj("B const& arg")C obj("C const& arg")

因此,这些模板构造函数将进入寻找可行候选者的阶段,此时将根据 [over.match.viable]/3 检查约束。

对于 和它的构造函数,候选者(包括约束)是BB(const T&)

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

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

std::is_copy_constructible_v<B>

在完全解决第一个检查(即递归)之前,我们会遇到对同一特征的约束满足度检查。我无法找到拒绝此类程序的明确措辞,但按理说,重载解析期间的递归应该会导致格式错误的程序。因此,Clang 拒绝这个例子可以说是正确的。B

对于 和它的构造函数,候选者(包括约束)是CC(T&&)

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

与示例相反,这不会导致递归,因为总是 true(对于任何可引用的类型,例如 any but 或 cv-/ref-qualified 函数类型)。Bstd::is_copy_constructible_v<C const&>Cvoid

因此,所有编译器都可以说接受这个例子是正确的。C

评论

2赞 Jarod42 1/27/2022
但是可以选择模板构造函数而不是移动/复制构造函数 Demo。所以 [class.copy.ctor]/5 阻止了 OP 情况下的实例化(所以 clang 错误),但它也发生在([class.copy.ctor]/5 不适用)......const T&
0赞 dfrib 1/27/2022
@Jarod42 是的,类型的参数(如您的演示中)将匹配移动或复制 ctor 上的(通用/转发引用)ctor,但特征不会:它尝试匹配 type 的参数,该参数将选择复制 ctor。即使对于存在模板 ctor 的情况,复制 ctor 也是一个明确更好的匹配A&T&&std::is_copy_constructibleA const&T const&
0赞 Jarod42 1/27/2022
我们同意复制构造函数是一个更好的匹配,但之前没有实例化以查看它是否是一个可行的候选者?const T&
0赞 dfrib 1/27/2022
@Jarod42 同意,但这在任何情况下都不应该是错误的(、、),对吧?特别是,导致非常量表达式的 clang 错误诊断似乎是无稽之谈(与在 constexpr 上下文中调用此类函数相比,将非 constexpr 函数视为重载解析中的可行候选函数肯定是不同的)。TT const&T&&std::is_copy_constructible_v<T>
1赞 Jarod42 1/27/2022
确实,应该是完整的,但仍然有可能无限递归,我认为这是编译器的合法实现。我认为编译器可能会省略(未来)不是最佳匹配的实例化。A