【问题标题】:Why isn't the most appropriate constructor called in this case?为什么在这种情况下不调用最合适的构造函数?
【发布时间】:2016-05-21 09:38:26
【问题描述】:

考虑以下类:

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 的转换,这是一个完全有效的路径,但我无法理解它。为什么编译器会拒绝简单地调用与提供的值具有完全相同的参数类型的构造函数?这不是最简单的事情,因此是默认行为吗?调用转换运算符也会执行复制,所以我不认为这比简单地调用复制构造函数更快或更优化。

【问题讨论】:

  • 不应该是 is_constructible 而不是你目前拥有的其他方式吗?
  • @Alejandro 正确,那也是solves the issue
  • @nwp 我很困惑。根据this,该事物返回是否可以从第二事物构造第一个事物。由于我正在从 T 参数构造我的 data 成员(这是一个 int),因此这应该是正确的顺序。我只接受可以构造 int 的类型。
  • 你说的确实解决了问题,但我现在比以前更困惑:D
  • @Alejandro 所说的仅有效,因为 T 在示例中是 int 。如果您将数据声明为任何非原始类型(称为 bar)并且 int 不能从 bar 构造,那么这些构造函数将从 is_constructible 中排除,因为对于 T : int 然后 int 没有可见的方法来构造自身从酒吧。但是 is_constructible for T :如果 bar 声明了一个接受 int 的公共构造函数, int 将成功。如果我对用法的解释有误,请纠正我。

标签: c++ constructor type-conversion copy-constructor enable-if


【解决方案1】:

您的模板“移动”构造函数(带有T = foo &amp;)有一个foo &amp; 类型的参数,它比您的复制构造函数更匹配,因为它只需要const foo &amp;。然后,您的模板构造函数通过调用 operator int()i 转换为 int 来填充 data

最简单的直接修复可能是使用enable_if 来限制移动构造函数进行移动操作:如果T 被推断为左值引用类型(意味着你的T&amp;&amp; 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&lt;int, T&gt;::value 是一个测试,它告诉您data(std::forward&lt;T&gt;(i)) 是否格式正确。它测试data{std::forward&lt;T&gt;(i)} 是否格式正确。结果不同的Tlong,因为longint 的转换是缩窄转换,{} 中不允许缩窄转换。

【讨论】:

  • 这似乎是一个可能的解决方案,但我不确定你的意思。你能解释清楚一点吗?
  • 这行得通!我不知道右值引用会以任何方式折叠为左值引用。谢谢!
  • 您添加的其他内容也非常有用。有些事情我没有想到。谢谢
  • 我刚刚看了this 的谈话...现在一切都说得通了 :) Herb 在谈话的最后一个季度谈到了T&amp;&amp; 的完美转发。我希望我早点看过这个。
【解决方案2】:

这是因为operator int()

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


可能的解决方案:

  1. 使用static_cast&lt;foo&gt;。这会将通常应该转换为int 的变量转换为foo

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

【讨论】:

  • @Alejandro 说的也是真的。
  • 我知道为什么会这样,但问题是为什么会这样。由于有一个构造函数采用参数的确切类型 (foo),因此不应进行任何转换。如果该类没有接受foo 对象的构造函数,这将是完全正常的,但既然如此,那应该是比触发转换更简单的途径。另外,static_cast 也不能解决问题。我传入的值已经是foo类型的了,不用再强制转换了。
  • 那么也许你可以创建一个operator foo()
  • 所以我的问题是我将 foo 对象传递给一个函数,该函数确实有一个 foo 参数的重载,但这不是被调用的重载。
  • 呃,我真的很困惑。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-20
  • 1970-01-01
  • 2022-10-15
  • 1970-01-01
  • 2018-12-06
相关资源
最近更新 更多