为move调用转换构造函数,但未为copy调用转换构造函数

Conversion constructor called for move but not for copy

本文关键字:转换 构造函数 调用 copy move      更新时间:2023-10-16

使用以下代码:

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)

move转换构造函数即使对相同的类型也被调用,为什么?

是否有办法统一复制和转换构造函数以避免重复代码?

添加隐式生成(=编译器生成)移动构造函数的规则非常保守:只在安全的情况下添加一个。如果用户已经定义了复制构造函数,编译器不能认为添加一些简单的编译器生成的移动构造函数仍然是安全的,因此不会添加移动构造函数。实际上,声明它足以防止生成move构造函数,IIRC允许仅复制类型。

构造函数模板既不被认为是复制构造函数,也不被认为是移动构造函数,所以它们不阻止它们的隐式生成。

当然,如果从未声明move构造函数,则重载解析可以选择另一个构造函数。考虑这个例子:
#include <iostream>
#include <utility>
struct loud
{
    loud() { std::cout << "default ctorn"; }
    loud(loud const&) { std::cout << "copy ctorn"; }
    loud(loud&&) { std::cout << "move ctorn"; }
};
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));
}
输出:

<>之前默认的男星复制男星移动男星--------------默认的男星复制男星复制男星之前

这里,foo将隐式声明默认、复制和移动构造函数,而bar将不会隐式声明移动构造函数,因为它有用户声明的复制构造函数


在第二种情况下,没有隐式声明的move构造函数,因此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;
    };