Class with constructor template requiring copyable argument is not copyable by itself
Asked Answered
V

1

4

In the following program struct A has a constructor template A(T) requiring that the type T be copy-constructible. At the same time A itself must have implicitly defined copy-constructor:

#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>);

and the last static_assert(std::is_copy_constructible_v<A>) passes in GCC and MSVC, but Clang rejects it, complaining:

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...)>
                             ^~~~~~~~~~~~~~~~~~
...

Demo: https://gcc.godbolt.org/z/shKe7W1jr

Is it just a Clang bug?

Valet answered 27/1, 2022 at 7:19 Comment(7)
Yes it is. The template is not even allowed to instantiate as a copy constructor.Joycelynjoye
@PasserBy: Shouldn't it be instantiated, before being rejected? And as class is still incomplete, the clause cannot be known...Might
@Might See [class.copy.ctor].Joycelynjoye
@PasserBy: That handles OP's case. (but issue persists with const T& (Demo), T&, (but not with T&& (Demo)))...Might
@Might Clang is wrong in all cases as a template constructor, no matter the cv- and ref-qualifiers of its parameter(s), will never suppress the generation of the implicitly-declared move/copy constructors and assignment ops. [class.copy.ctor]/5 covers the corner case of a recursive constructor (never an issue for a templated S(T) constructor).Bibliopegy
@dfrib: I didn't say implicit generation are suppressed, but all overload should be taken into account. ([class.copy.ctor]/5 indeed remove issue with OP's code, but not its variation).Might
@Might Ah I see, my bad. I don't understand clang's "initializer of 'is_copy_constructible_v<A>' is unknown" error, as the point of instantiation (temp.point/1) of the template constructor follows after A is complete.Bibliopegy
B
2

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.

Bibliopegy answered 27/1, 2022 at 11:24 Comment(9)
But template constructor can be selected instead of move/copy constructor Demo. So [class.copy.ctor]/5 prevents instantiation in OP case (so clang bug), but it also happens for const T& ([class.copy.ctor]/5 doesn't apply)...Might
@Might Yes an argument of type A& (as in your demo) will match a T&& (universal/forwarding ref) ctor over the move or copy ctor, but the std::is_copy_constructible trait will not: it attempts to match an argument of type A const&, which will pick the copy ctor. Even for the case where a template T const& ctor exists the copy ctor is a non-ambiguously better match.Bibliopegy
We agree copy constructor is a better match, but doesn't const T& be instantiated before to see if it is a viable candidate?Might
@Might Agreed, but this should not be an error in any of the cases (T, T const&, T&&), right? Particularly the clang error diagnosing that std::is_copy_constructible_v<T> results in a non-constant expression seems like nonsense (considering a non-constexpr function as a viable candidate in overload resolution as compared to invoking such a function in a constexpr context are certainly different).Bibliopegy
Not sure it is not an issue, A is not complete yet, and std::is_copy_constructible_v<A> would also produce infinite recursion. Adding more constraint fixes the issue too Demo.Might
@Might As I interpret it, [temp.point]/1 indicates the point of instantiation of the template ctor follows after As completion. Indeed, recursive overload resolution sets via checking the std::is_copy_constructible_v<A> constraint sounds like a plausible root cause for what appears to be a clang bug, but I know there are subtleties/underspecification in the domain of "when" constraints are checked and whether and how they are cached. Maybe this case does not fall under this domain though, as recursive overload sets would mean we never exit the first constraint check.Bibliopegy
Indeed, A should be complete, but there is still possible infinite recursion, which I think is legal implementation from compiler. I think compiler might omit instantiation for (future) not best match.Might
@Might I expanded the answer with a novel and I'd argue that Clang is wrong to reject A(T), GCC & MVSC are wrong to accept A(const T&) and all of them are correct to accept A(T&&).Bibliopegy
Thanks. Another interesting question, shall anything change if one replaces std::is_copy_constructible_v with the concept std::copy_constructible? Example: gcc.godbolt.org/z/fejocE4svValet

© 2022 - 2024 — McMap. All rights reserved.