为什么在这种情况下没有调用最合适的构造函数?

Why isn't the most appropriate constructor called in this case?

考虑以下 class:

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

当然,拿一个int作为任何一种参考意义不大,但这只是一个例子。 data 成员的复制成本可能非常高,因此所有移动语义。

那个花哨的模板基本上支持可以构造 data 的任何类型。因此,foo 对象可以通过复制或移动满足此条件的任何类型的值来构造,或者简单地通过复制或移动类型 foo 的另一个对象来构造。到目前为止非常简单。

当您尝试执行如下操作时出现问题:

foo obj1(42);
foo obj2(obj1);

应该做的(至少在我看来)是通过将值 42 移入第一个对象(因为它是右值)来构造第一个对象,然后构造通过复制第一个对象来创建第二个对象。所以这应该打印出来的是:

Value move ctor
Copy ctor

但是实际打印出来的是:

Value move ctor
Operator int
Value move ctor

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

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

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

它的发生是因为 operator int().

因为 operator int()obj1 调用 operator int() 并将自己转换为 int


可能的解决方案:

  1. 使用static_cast<foo>。这会将通常应转换为 int 的变量转换为 foo.

  2. 任何人请编辑此问题并填写。我没有更多的想法了。

你的模板 "move" 构造函数(带 T = foo &)有一个 foo & 类型的参数,它比你的复制构造函数更匹配,因为它只需要 const foo &.然后,您的模板构造函数通过将 i 转换为 int,调用 operator int().

来填充 data

最简单的即时修复可能是使用 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; }

注意:由于您的参数在构造函数中有一个名称,就像所有其他命名对象一样,它是一个左值。鉴于你想从它移动,你可以使用 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; }

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

另一个注意事项:is_constructible<int, T>::value 是一个测试,告诉您 data(std::forward<T>(i)) 是否合式。它 not 测试 data{std::forward<T>(i)} 是否合式。结果不同的 Tlong,因为 longint 的转换是缩小转换,而 {} 不允许缩小转换.