为什么在这种情况下不是最合适的构造函数?

ada*_*603 4 c++ constructor type-conversion copy-constructor enable-if

考虑以下课程:

class foo {
    int data;
public:
    template <typename T, typename = enable_if_t<is_constructible<int, T>::value>>
    foo(const T& i) : data{ i } { cout << "Value copy ctor" << endl; }

    template <typename T, typename = enable_if_t<is_constructible<int, T>::value>>
    foo(T&& i) : data{ i } { cout << "Value move ctor" << endl; }

    foo(const foo& other) : data{ other.data } { cout << "Copy ctor" << endl; }

    foo(foo&& other) : data{ other.data } { cout << "Move ctor" << endl; }

    operator int() { cout << "Operator int()" << endl; return data; }
};
Run Code Online (Sandbox Code Playgroud)

当然,int通过任何类型的引用都没有多大意义,但这只是一个例子.该data成员可能是复制非常昂贵,因此所有的移动语义.

这个花哨的模板基本上可以data构建任何可以构造的类型.因此,foo可以通过复制或移动满足此条件的任何类型的值,或者仅通过复制或移动另一个类型的对象来构造对象foo.到目前为止非常直截了当.

当您尝试执行以下操作时会发生此问题:

foo obj1(42);
foo obj2(obj1);
Run Code Online (Sandbox Code Playgroud)

该做什么(至少在我看来)是通过将值42移入其中来构造第一个对象(因为它是一个右值),然后通过复制第一个对象来构造第二个对象.那么打印出来的是:

Value move ctor
Copy ctor
Run Code Online (Sandbox Code Playgroud)

但实际打印出来的是:

Value move ctor
Operator int
Value move ctor
Run Code Online (Sandbox Code Playgroud)

第一个对象构造得很好,没问题.但是,不是调用复制构造函数来构造第二个对象,程序将第一个对象转换为另一个类型(通过我们定义的转换),然后调用另一个构造函数foo,可以从该类型移动(因为它是该点的右值) .

我觉得这很奇怪,这绝对不是我想从这段代码中得到的行为.我认为在构造第二个对象时调用复制构造函数更有意义,因为考虑到我提供的参数类型,这似乎更简单.

谁能解释一下这里发生了什么?当然我理解,因为有一个用户定义的转换为int,这是一个非常有效的路径,但我无法理解它.为什么编译器拒绝简单地调用与提供的值具有完全相同的参数类型的构造函数?这不是最琐碎的事情,因此默认行为?调用转换运算符也会执行副本,所以我认为这不比简单地调用复制构造函数更快或更优.

小智 5

你的模板"move"构造函数(with T = foo &)有一个类型的参数foo &,这是一个比你的拷贝构造函数更好的匹配,因为它只需要const foo &.然后,您的模板构造函数将data通过转换iint,调用来填充operator int().

最简单的立即修复可能是用来enable_if限制你的移动构造函数移动操作:如果T推断为左值引用类型(意味着你T&& i也会崩溃到左值引用),强制替换失败.

template <typename T, typename = enable_if_t<is_constructible<int, T>::value>,
                      typename = enable_if_t<!is_lvalue_reference<T>::value>>
foo(T&& i) : data( std::move(i) ) { cout << "Value move ctor" << endl; }
Run Code Online (Sandbox Code Playgroud)

注意:由于您的参数在构造函数中有一个名称,就像所有其他命名对象一样,它是一个左值.鉴于您想要从中移动,您可以使用std::move.

更一般地说,您可以使用完美转发(接受左值和右值),并仅将foo其自身作为特殊例外删除:

template <typename T, typename = enable_if_t<is_constructible<int, T>::value>
                    , typename = enable_if_t<!is_same<decay_t<T>, foo>::value>
foo(T&& i) : data( std::forward<T>(i) ) { cout << "Forwarding ctor" << endl; }
Run Code Online (Sandbox Code Playgroud)

这将替换您的值复制值移动构造函数.

另一个注意事项:is_constructible<int, T>::value是一个测试,告诉你是否data(std::forward<T>(i))将是良好的形式.它并没有测试是否data{std::forward<T>(i)}是良好的.甲T的量,结果不同的是long,由于longint转换是收缩转换,以及缩小转换中不允许{}.