【问题标题】:Universal reference argument used twice通用引用参数使用了两次
【发布时间】:2023-03-02 23:44:01
【问题描述】:

我希望围绕 std::make_pair 创建一个包装器,它接受一个参数并使用该参数来生成该对的第一个和第二个成员。此外,我希望利用移动语义。

天真地,我们可能会写(为了清楚起见忽略返回类型),

template <typename T>
void foo(T&& t)
{
  std::make_pair(std::forward<T>(t),
                 std::forward<T>(t));
}

但这不太可能达到我们想要的效果。

我们想要的是:

  • 在使用 (const) 左值引用参数调用 foo 的情况下,我们应该将该 (const) 引用传递给 std::make_pair 而不修改两个参数。
  • 在使用右值引用参数调用 foo 的情况下,我们应该复制被引用的对象,然后使用原始右值引用以及对新创建对象的右值引用调用 std::make_pair。

到目前为止,我想出的是:

template <typename T>
T forward_or_duplicate(T t)
{
  return t;
}

template <typename T>
void foo(T&& t)
{
  std::make_pair(std::forward<T>(t),
                 forward_or_duplicate<T>(t));
}

但我有理由确定这是错误的。

所以,问题:

  1. 这行得通吗?我怀疑不是因为如果使用右值引用调用 foo() 则在构造按值传递给 forward_or_duplicate() 的 T 时将调用 T 的移动构造函数(如果存在),从而破坏 t。

  2. 即使它确实有效,它是最优的吗?同样,我怀疑不会在从 forward_or_duplicate() 返回 t 时调用 T 的复制构造函数。

  3. 这似乎是一个常见问题。有没有惯用的解决方案?

【问题讨论】:

  • 如果参数评估之间的顺序已定义(不幸的是,它没有),我认为您只需 std::make_pair(t, std::forward&lt;T&gt;(t)) 就可以做到这一点。
  • @KerrekSB:哈,没想到。你应该提交一个答案:-)
  • 其实我看不出forward_or_duplicate有什么问题。
  • @KerrekSB 我没看到。 make_pair 通过引用获取参数,因此移动发生在其中,复制发生在您输入 make_pair 之前。
  • @KerrekSB 它转发左值,并复制右值。

标签: c++ c++11 perfect-forwarding


【解决方案1】:

所以,问题:

  1. 这行得通吗?我怀疑不是因为如果使用右值引用调用 foo() 则 T 的移动构造函数(如果存在)将是 在构造按值传递的 T 时调用 forward_or_duplicate(),从而破坏了 t。

不,foo中的t是一个左值,所以构造T通过值传递给 forward_or_duplicate() from t 调用复制构造函数。

  1. 即使它确实有效,它是最优的吗?同样,我怀疑不会在从返回 t 时调用 T 的复制构造函数 forward_or_duplicate()。

不,t 是一个函数参数,所以 return 隐式移动,而不是复制。

也就是说,这个版本会更高效、更安全:

template <typename T>
T forward_or_duplicate(std::remove_reference_t<T>& t)
{
  return t;
}

如果T 是左值引用,则会产生与以前相同的签名。如果T 不是参考,这可以为您节省一步。此外,它会将T 放入非推断上下文中,这样您就不会忘记指定它。

【讨论】:

  • 谢谢。这回答了我最初的问题。在我最初的解决方案中,我有 "T forward_or_duplicate(typename std::enable_if::type t)" 将 T 放入非推导上下文中,但为了清楚起见,从问题中省略了它。
【解决方案2】:

您的确切代码有效。它的细微变化(即,不调用make_pair 而是调用其他函数)会导致未指定的结果。即使它看起来有效,远离这行代码的细微更改(本地正确)也可能破坏它。

您的解决方案不是最优的,因为它可以复制 T 两次,即使它可以工作,而它只需要复制一次。


这是迄今为止最简单的解决方案。它不能解决由其他地方的代码更改引起的细微中断,但如果您真的在调用 make_pair,这不是问题:

template <typename T>
void foo(T&& t) {
  std::make_pair(std::forward<T>(t),
             static_cast<T>(t));
}

static_cast&lt;T&gt;(t) 对于推导类型,如果T&amp;&amp; 是左值,则T&amp;&amp; 是noop,如果T&amp;&amp; 是右值,则为副本。

当然,static_cast&lt;T&amp;&amp;&gt;(t) 也可以用来代替std::forward&lt;T&gt;(t),但人们也不会这样做。

我经常这样做:

template <typename T>
void foo(T&& t) {
  T t2 = t;
  std::make_pair(std::forward<T>(t),
             std::forward<T>(t2));
}

但这会阻止理论上的省略机会(此处不会出现)。

一般来说,在与static_cast&lt;T&gt;(t) 或任何等效的复制或转发函数相同的函数调用上调用std::forward&lt;T&gt;(t) 是一个坏主意。未指定评估参数的顺序,因此如果使用std::forward&lt;T&gt;(t) 的参数不是T&amp;&amp; 类型,并且其构造函数看到右值T 并将状态移出它,static_cast&lt;T&gt;(t) 可以评估 在t的状态被撕掉之后。

这里不会发生这种情况:

template <typename T>
void foo(T&& t) {
  T t2 = t;
  std::make_pair(std::forward<T>(t),
             std::forward<T>(t2));
}

因为我们将复制或转发移动到不同的行,在那里我们初始化t2

虽然T t2=t; 看起来总是复制,但如果T&amp;&amp; 是左值引用,T 也是左值引用,int&amp; t2 = t; 不会复制。

【讨论】:

    猜你喜欢
    • 2011-01-22
    • 2013-07-09
    • 1970-01-01
    • 2021-02-11
    • 1970-01-01
    • 2023-03-15
    • 2014-01-01
    • 2017-06-25
    • 2013-01-21
    相关资源
    最近更新 更多