TLDR
Given the following examples A
(OP's example), B
and 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
Then:
A
with #OP
is well-formed
- [rejects-invalid] Clang is wrong to reject it
- [accepts-valid] GCC and MVSC is correct to accept it
B
with #i
is arguably ill-formed due recursion during overload resolution
- [rejects-valid] Clang is correct to reject it
- [accepts-invalid] GCC and MVSC are arguably incorrect to accept it
C
with #ii
is well-formed
- [accepts-valid] Clang, GCC and MVSC are correct to accept it
Details
First of all, as per [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, are not affected by template constructors.
/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.
This means that OP's program may not be rejected as ill-formed because the class A
is not copy constructible. That leaves the program being ill-formed due errors during overload resolution, particularly triggered by static_assert(std::is_copy_constructible_v<A>);
, which as part of the trait will try to construct a type A
with an argument of type A const&
. Even if this is done in an unevaluated context, it will trigger overload resolution where all constructors of A
, including the template constructor, are candidates. Simplified:
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 ?
As per [over.match.funcs]/7 the template ctor is a candidate
In each case where a candidate is a function template, candidate function template specializations are generated using template argument deduction ([temp.over], [temp.deduct]) [...]
However the resulting candidate function template specialization would be (the recursive constructor) A(A)
and as per [class.copy.ctor]/5 a template constructor will never be used produce such a specialization:
/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.
Thus a specialization of the template constructor never even enters the set of candidate functions, meaning we never reach neither the state of rejecting candidates as per the usual overload resolution rules, nor the state of rejecting candidates due to failed constraints.
Thus, as overload resolution contains only the implicitly generated copy and move constructors, Clang is wrong to reject OP's program.
As pointed out by @Jarod42, a more interesting example is when the template constructor has an argument T const&
or T&&
(lvalue const reference and universal/forwarding reference, respectively):
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
Curiosly, whilst GCC and MVSC accepts both these cases, Clang rejects #i
with the same error messages as for OP's example, but accepts #ii
.
For the template constructors of classes B
and C
, the special case of class.copy.ctor]/5 does not apply, meaning a specialization for both #i
and #ii
would enter the candidate set of the unevaluated call B obj("B const& arg")
and C obj("C const& arg")
, respectively.
Thus, these template constructors will enter the phase of finding viable candidates, at which point constraints will be checked, as per [over.match.viable]/3.
For B
and its B(const T&)
constructor, the candidate, including constraints, is
// T = B
B(const B&) requires requires(std::is_copy_constructible_v<B>)
Meaning that as part of checking the trait (recall the static assert)
std::is_copy_constructible_v<B>
we run into a constraint satisfaction check over the same trait, before completely resolving the first check, meaning recursion. I have not been able to find the explicit wording which would reject such a program, but it stands to reason that recursion during overload resolution should result in an ill-formed program. Thus, Clang is arguably correct to reject the B
example.
For C
and its C(T&&)
constructor, the candidate, including constraints, is
// T = C const&
C(C const&) requires requires(std::is_copy_constructible_v<C const&>)
As opposed to the example of B
, this does not lead to recursion as std::is_copy_constructible_v<C const&>
is always true (true for any type C
that is referenceable, e.g. anything but void
or cv-/ref-qualified function types).
Thus, all compilers are arguably correct to accept the C
example.
const T&
(Demo),T&
, (but not withT&&
(Demo)))... – MightS(T)
constructor). – BibliopegyA
is complete. – Bibliopegy