C++ Template Copy Constructor on Template Class

Copy constructor of template class

There are strict rules what constitutes a copy constructor (cf. C++11, 12.8):

  • It is not a template.

  • For a class T, its first argument must have type T & or T const & or T volatile & or T const volatile &.

  • If it has more than one argument, the further arguments must have default values.

If you do not declare a copy constructor, a copy constructor of the form T::T(T const &) is implicitly declared for you. (It may or may not actually be defined, and if it is defined it may be defined as deleted.)

(The usual overload resolution rules imply that you can have at most four copy constructors, one for each CV-qualification.)

There are analogous rules for move constructors, with && in place of &.

C++ template copy constructor on template class

A copy constructor is of the form X(X& ) or (X const&) and will be provided for you by the compiler if you didn't declare one yourself (or a few other conditions which are not relevant here). You didn't, so implicitly we have the following set of candidates:

MyTemplateClass(const MyTemplateClass&);
template <typename U> MyTemplateClass(const MyTemplateClass<U>&);

Both are viable for

MyTemplateClass<int> instance2(instance);

Both take the same exact arguments. The issue isn't that your copy constructor template doesn't match. The issue is that the implicit copy constructor is not a function template, and non-templates are preferred to template specializations when it comes to overload resolution. From [over.match.best], omitting the unrelated bullet points:

Given these definitions, a viable function F1 is defined to be a better function than another viable function
F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then

— [...]

— F1 is not a function template specialization and F2 is a function template specialization, or, if not that,

— [...]

That's why it calls your implicit (and then, your explicit) copy constructor over your constructor template.

Why doesn't the standard consider a template constructor as a copy constructor?

Let's put templates aside for a second. If a class doesn't declare a copy constructor, an implicitly defaulted one is generated. It may be defined as deleted, but it's defaulted nonetheless.

A member template is not a member function. Members are instantiated from it only when needed.

So how can a compiler know from the class definition alone whether or not a specialization with T = Foo will ever be needed? It can't. But it's exactly that on which it needs to base a decision of how to handle a potential need for an implicitly defaulted copy constructor (AND move constructor). That becomes messy.

The easiest approach is to exclude templates. We'll always have some copy constructor anyway, it will do the correct thingTM by default, and will be favored by overload resolution because it's not instantiated from a template.

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

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.

Trouble with a copy-constructor in C++ using templates

The problem is that currently you have overloaded operator[] as a non-const member function of class template CB. This means that the implicit this parameter of this member function is of type CB<DataType>*. Meaning that we can use this member function only with non-const objects.

To solve this problem you need to make it(overload it as) a const member function instead by adding a const as shown below, so that now the implicit this parameter will be of type const CB<DataType>* meaning now it can be used with const objects.

template <class DataType>
class CB : public CA <CA <DataType> >
{

virtual CA<DataType>& operator[] (int index) const; //added const here
//other members here
}
template <class DataType>
CA<DataType>& CB<DataType>::operator[] (int index) const //added const here
{
return (*(*theRows)[index]);
}
//other code here

The program compiles after the modification as can be seen here.

Add copy constructor based on template parameters

struct nonesuch {
private:
~nonesuch();
nonesuch(const nonesuch&);
void operator=(const nonesuch&);
};

template<bool IsCopyable>
struct Foo {
Foo(const typename std::conditional<IsCopyable, Foo, nonesuch>::type& other) {
// copy ctor impl here
}
private:
Foo(const typename std::conditional<!IsCopyable, Foo, nonesuch>::type&);
};

Likewise for assignment.

A uniform copy and move constructor of a template class

Your choices are essentially these:

  1. Take the parameter by value:

    DataProcessor(DataPointer p) : pData(std::move(p)) {}

    If DataPointer is move-only, then the user will have to call it through std::move, which will move-construct p, which is then used to move-construct pData. If it is copyable, then p will be copy/move constructed based on how the user passes the value. From there, it will move construct pData.

    Note that this version adds an additional move operation.

  2. Take the parameter by rvalue reference, always:

    DataProcessor(DataPointer &&p) : pData(std::move(p)) {}

    In this case, if DataPointer is not move-only, and the user wants to pass an lvalue, the user must explicitly copy the value into a temporary used to initialize p. That would look something like this:

    DataProcessor<shared_ptr<T>> dp(shared_ptr{sp});

    Where sp is an existing shared_ptr that you want to copy from. This does only one move when given an object to move from, but does a copy+move when copying.

  3. Write two functions, employing SFINAE to remove the copying version if DataPointer is non-copyable. This version has the advantage of doing no additional moves:

DataProcessor(DataPointer &&p) : pData(std::move(p)) {}
template<typename T = DataPointer>
DataProcessor(std::enable_if_t<std::is_copy_constructible_v<T>, const T&> p)
: pData(p) {}

Why template class assignment operator can use templated copy constructor to assign different type

Just like @Jarod42 said in the comments. I used cppinsights.io and realised that the compiler is seeing c2 = c1 as c2.operator=(Container<float>(c1));, so I suppose it is simply looking for a conversion constructor (what we called a "templated copy constructor" earlier), to see if there is any known way to cast one type to the other.

How to write a copy constructor for Template Class - C++

NOTE: You declare an assignment operator, not copy constructor

  1. You missed the const qualifier before return type
  2. You missed the template argument(<T>) for return type and function argument

Use this:

template <typename T>
const Vector<T>& Vector<T>::operator=(const Vector<T>& rhs)


Related Topics



Leave a reply



Submit