Конструктор преобразования вызывается для перемещения, но не для копирования

Используя этот код:

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;
    };

};

С этим тестовым кодом:

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

Производит этот вывод:

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

Конструкторы копирования и перемещения по умолчанию используются вместо конструкторов преобразования при использовании тех же типов.

Но если я добавлю в класс конструктор копирования по умолчанию:

Test(Test const &test) = default;

Он производит этот вывод:

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

Конструктор преобразования перемещения вызывается даже с одинаковыми типами, почему?

Есть ли способ унифицировать конструктор копирования и преобразования, чтобы избежать дублирования кода?


person Johnmph    schedule 30.03.2014    source источник


Ответы (1)


Правила добавления неявно сгенерированных (= сгенерированных компилятором) конструкторов перемещения довольно консервативны: добавляйте только один, если это безопасно. Если конструктор копирования был определен пользователем, компилятор не может предположить, что добавление какого-либо простого конструктора перемещения, сгенерированного компилятором, по-прежнему безопасно, поэтому конструктор перемещения добавлен не будет. На самом деле достаточно объявить его, чтобы предотвратить создание конструктора перемещения, IIRC, чтобы разрешить типы только для копирования.

Шаблоны конструкторов никогда не рассматриваются ни как конструкторы копирования, ни как конструкторы перемещения, поэтому они не препятствуют их неявному созданию.

Конечно, если конструктор перемещения никогда не объявлялся, вместо него можно выбрать другой конструктор с помощью разрешения перегрузки. Рассмотрим этот пример:

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

Выход:

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

Здесь foo получит неявно объявленный конструктор по умолчанию, копирование и перемещение, тогда как bar не получит неявно объявленный конструктор перемещения, поскольку он имеет объявленный пользователем конструктор копирования.


Во втором случае нет неявно объявленного конструктора перемещения, поэтому разрешение перегрузки для Test<int> testInt4(std::move(testInt2)) предпочитает шаблон конструктора конструктору копирования, поскольку первый использует ссылку с меньшей квалификацией cv.


Чтобы уменьшить дублирование кода, вы можете делегировать создание:

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;
    };

};

Кстати, вы также можете уменьшить некоторые шаблоны, используя шаблон конструктора с идеальной переадресацией вместо двух шаблонов конструктора:

Используя эту черту:

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

Вы можете определить:

    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;
    };
person dyp    schedule 30.03.2014
comment
Большое спасибо, мне нужно изучить идеальный прием вперед, но у меня другой вопрос, вы сказали, что шаблон конструктора вызывается, потому что конструктор перемещения не добавляется компилятором, потому что есть пользовательский (декларированный) конструктор копирования . Но если я удалю конструктор копирования (=delete вместо = default), будет ошибка компиляции, шаблон конструктора использоваться не будет, почему? - person Johnmph; 31.03.2014
comment
@Johnmph Хм, не уверен, что ты имеешь в виду. Явное удаление (с помощью = delete) функции на самом деле означает, что эта функция недействительна, и если кто-то попытается ее вызвать, возникнет ошибка (во время компиляции). Это не означает, что этой функции не существует. Поэтому, если вы явно удалите конструктор копирования, его все равно можно будет выбрать с помощью разрешения перегрузки. Если это произойдет, компилятор будет жаловаться. (И вы не можете заставить конструктор копирования исчезнуть, как перемещение ctor.) - person dyp; 31.03.2014
comment
Спасибо, теперь я понял, у меня есть последний вопрос, пожалуйста, не могли бы вы сказать мне, добавляет ли ваше решение с тегом struct, позволяющим построение делегата, некоторые накладные расходы в скомпилированном коде или оно используется только компилятором, и накладные расходы не добавляются? - person Johnmph; 01.04.2014
comment
Я не могу дать вам какие-либо гарантии на этот счет, но если вы компилируете с оптимизацией, компилятор должен иметь возможность удалить этот параметр tag: он не используется, пуст и функция встроена/определена в каждом переводе. Ед. изм. - person dyp; 01.04.2014