Question

Using this code :

template <class T> class Test {
    T _temp;

public:
    Test() {
        std::cout << "Test()" << std::endl;
    };

    template <class T2> Test(Test<T2> const &test) {
        std::cout << "template <class T2> Test(Test<T2> const &test)" << std::endl;
    };

    template <class T2> Test(Test<T2> &&test) {
        std::cout << "template <class T2> Test(Test<T2> &&test)" << std::endl;
    };

};

With this test code :

Test<int> testInt;
Test<float> testFloat(testInt);
Test<float> testFloat2(std::move(testInt));

std::cout << "----------" << std::endl;

Test<int> testInt2;
Test<int> testInt3(testInt2);
Test<int> testInt4(std::move(testInt2));

Produces this output :

Test()
template <class T2> Test(Test<T2> const &test)
template <class T2> Test(Test<T2> &&test)
----------
Test()

The default copy and move constructors are used instead conversion constructors when using the same types.

But if I add default copy constructor in the class :

Test(Test const &test) = default;

It produces this output :

Test()
template <class T2> Test(Test<T2> const &test)
template <class T2> Test(Test<T2> &&test)
----------
Test()
template <class T2> Test(Test<T2> &&test)

The move conversion constructor is called even with the same types, why ?

Is there a way to unify the copy and conversion constructor to avoid duplicating code ?

Was it helpful?

Solution

The rules to add implicitly generated (= compiler-generated) move constructors are pretty conservative: Only add one if it's safe. If a copy constructor has been defined by the user, the compiler cannot assume that adding some simple compiler-generated move constructor is still safe, so no move constructor will be added. Actually, it's enough to declare it to prevent the generation of the move constructor, IIRC to allow copy-only types.

Constructor templates are never considered neither as copy nor as move constructors, so they don't prevent the implicit generation of those.

Of course, if the move constructor is never declared, another constructor can be chosen by overload resolution instead. Consider this example:

#include <iostream>
#include <utility>

struct loud
{
    loud() { std::cout << "default ctor\n"; }
    loud(loud const&) { std::cout << "copy ctor\n"; }
    loud(loud&&) { std::cout << "move ctor\n"; }
};

struct foo
{
    loud l;
};

struct bar
{
    loud l;
    bar() = default;
    bar(bar const&) = default;
};

int main()
{
    foo f0;
    foo f1(f0);
    foo f2(std::move(f0));

    std::cout << "--------------\n";

    bar b0;
    bar b1(b0);
    bar b2(std::move(b0));
}

Output:

default ctor
copy ctor
move ctor
--------------
default ctor
copy ctor
copy ctor

Here, foo will get implicitly declared default, copy and move constructors, whereas bar won't get an implicitly declared move constructor, since it has user-declared copy constructor.


In your second case, there's no implicitly-declared move constructor, therefore overload resolution for Test<int> testInt4(std::move(testInt2)) prefers the constructor template to the copy constructor, since the former takes a reference that's less cv-qualified.


To reduce code duplication, you could delegate construction:

template <class T>
class Test {
    T _temp;

    struct tag {};

public:
    Test() {
        std::cout << "Test()" << std::endl;
    };

    Test(Test const& test)
        : Test(test, tag{})
    {}

    Test(Test&& test)
        : Test(std::move(test), tag{})
    {}

    template <class T2> Test(Test<T2> const &test, tag = {}) {
        std::cout << "template <class T2> Test(Test<T2> const &test)" << std::endl;
    };

    template <class T2> Test(Test<T2> &&test, tag = {}) {
        std::cout << "template <class T2> Test(Test<T2> &&test)" << std::endl;
    };

};

By the way, you can also reduce some boilerplate by using a perfectly forwarding constructor template instead of two constructor templates:

Using this trait:

template<class T>
struct is_Test : std::false_type {};
template<class T>
struct is_Test<Test<T>> : std::true_type {};

You can define:

    template <class T2,
              class = typename std::enable_if<is_Test<T2>::value>::type>
    Test(T2&& test, tag = {}) {
        std::cout << "template <class T2> Test(T2&& test)" << std::endl;
    };
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top