【问题标题】:Are there any use cases for std::forward with a prvalue?有纯右值的 std::forward 用例吗?
【发布时间】:2015-07-03 18:40:57
【问题描述】:

std::forward 最常见的用法是完善转发转发(通用)引用,例如

template<typename T>
void f(T&& param)
{
    g(std::forward<T>(param)); // perfect forward to g
}

这里paramlvaluestd::forward 最终将其转换为右值或左值,具体取决于绑定到它的参数是什么。

查看definition of std::forward from cppreference.com我看到还有rvalue重载

template< class T >
T&& forward( typename std::remove_reference<T>::type&& t );

谁能告诉我rvalue 超载的任何原因?我看不到任何用例。如果你想传递一个右值给一个函数,你可以直接传递它,不需要在上面应用std::forward

这与std::move 不同,我明白了为什么还需要rvalue 重载:您可能会处理不知道传递什么的通用代码,并且您希望无条件支持移动语义,参见例如Why does std::move take a universal reference?.

编辑为了澄清这个问题,我问为什么overload (2) from here 是必要的,以及它的用例。

【问题讨论】:

  • 存在将右值作为右值转发的用例。见N2951
  • 是不是为了防止右值被转发为左值而不使用一堆额外的 enable_if 条件? (见案例 C here
  • @wakjah 如何将右值作为左值转发?如果您调用f( prvalue ),其中prvalue 类似于get_value(),那么您传递一个rvalue,无需转发。也许我没有明白你的意思。
  • @DarioOO 感谢您的链接。你能写一个简洁的答案吗?从您的示例中,我仍然不清楚为什么还需要为右值定义 std::forward

标签: c++ c++11 move-semantics rvalue-reference


【解决方案1】:

好的,因为@vsoftco 要求简洁的用例,这里有一个精炼的版本(使用他的想法让“my_forward”实际看到调用了超载)。

我通过提供一个代码示例来解释“用例”,如果没有prvalue,它不会编译或行为不同(不管这是否真的有用)。

我们有2 overloads forstd::forward

#include <iostream>

template <class T>
inline T&& my_forward(typename std::remove_reference<T>::type& t) noexcept
{
    std::cout<<"overload 1"<<std::endl;
    return static_cast<T&&>(t);
}

template <class T>
inline T&& my_forward(typename std::remove_reference<T>::type&& t) noexcept
{
    std::cout<<"overload 2"<<std::endl;
    static_assert(!std::is_lvalue_reference<T>::value,
              "Can not forward an rvalue as an lvalue.");
    return static_cast<T&&>(t);
}

我们有 4 个可能的用例

用例 1

#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // &&
    Library( vector<int>&& a):b(std::move(a)){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    Library a( my_forward<vector<int>>(v)); // &
    return 0;
}

用例 2

#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // &&
    Library( vector<int>&& a):b(std::move(a)){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    Library a( my_forward<vector<int>>(std::move(v))); //&&
    return 0;
}

用例 3

#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // &
    Library( vector<int> a):b(a){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    Library a( my_forward<vector<int>>(v)); // &
    return 0;
}

用例 4

#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // &
    Library( vector<int> a):b(a){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    Library a( my_forward<vector<int>>(std::move(v))); //&&
    return 0;
}

这是一份简历

  1. 使用了重载 1,没有它会出现编译错误
  2. 使用了 Overload 2,没有它会出现编译错误
  3. 使用了重载 1,没有它会出现编译错误
  4. 使用了 Overload 2,没有它你会得到编译错误

注意,如果我们不使用forward

Library a( std::move(v));
//and
Library a( v);

你得到:

  1. 编译错误
  2. 编译
  3. 编译
  4. 编译

如您所见,如果您只使用两个 forward 重载中的一个,您基本上会导致无法编译 4 个案例中的 2 个,而如果您根本不使用 forward,您将只能编译 3 个共 4 例。

【讨论】:

  • 考虑到复杂的转发隐藏,我会称之为“不完美转发”。
【解决方案2】:

此答案用于回答@vsoftco 的评论

@DarioOO 感谢您的链接。你能写一个简洁的答案吗?从您的示例中,我仍然不清楚为什么还需要为右值定义 std::forward

简而言之:

因为没有右值特化,以下代码将无法编译

#include <utility>
#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // hi! only rvalue here :)
    Library( vector<int>&& a):b(std::move(a)){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    A a( forward<vector<int>>(v));
    return 0;
}

但是我忍不住要输入更多,所以这也是答案的不简洁版本。

加长版:

您需要移动v,因为Library 类没有接受左值的构造函数,只有右值引用。 如果没有完美的转发,我们最终会出现不良行为:

包装函数在传递重物时会导致高性能损失。

通过移动语义,我们确保在可能的情况下使用移动构造函数。 在上面的例子中,如果我们删除std::forward,代码将无法编译。

那么forward 到底在做什么呢?在没有我们共识的情况下移动元素?不!

它只是创建矢量的副本并移动它。我们怎么能确定呢?只需尝试访问该元素。

vector<int> v;
v.push_back(1);
A a( forward<vector<int>>(v)); //what happens here? make a copy and move
std::cout<<v[0];     // OK! std::forward just "adapted" our vector

如果您改为移动该元素

vector<int> v;
v.push_back(1);
A a( std::move(v)); //what happens here? just moved
std::cout<<v[0];  // OUCH! out of bounds exception

因此需要重载才能实现仍然安全的隐式转换,但如果没有重载则不可能。

实际上下面的代码将无法编译:

vector<int> v;
v.push_back(1);
A a( v); //try to copy, but not find a lvalue constructor

实际用例:

您可能会争辩说,转发参数可能会创建无用的副本,从而隐藏可能的性能损失,是的,这实际上是正确的,但请考虑实际用例:

template< typename Impl, typename... SmartPointers>
static std::shared_ptr<void> 
    instancesFactoryFunction( priv::Context * ctx){
        return std::static_pointer_cast<void>( std::make_shared<Impl>(

                std::forward< typename SmartPointers::pointerType>( 
            SmartPointers::resolve(ctx))... 
            )           );
}

代码取自我的框架(第 80 行):Infectorpp 2

在这种情况下,参数是从函数调用中转发的。无论Impl 的构造函数接受右值还是左值,SmartPointers::resolve 的返回值都会被正确移动(因此没有编译错误,并且无论如何都会被移动)。

基本上你可以在任何情况下使用std::foward,以使代码更简单、更易读,但你必须记住两点

  • 额外的编译时间(实际上没有那么多)
  • 可能会导致不需要的副本(当您没有明确地将某些内容移动到需要右值的内容中时)

如果小心使用是一个强大的工具。

【讨论】:

  • 谢谢,顺便说一句,您应该使用Library 而不是A。会考虑你的例子,特别是关于先复制然后移动。 AFAIK,forward 只是使用参考折叠规则进行投射。但无论如何,再次感谢,我会尽快让您知道我的想法。
  • 好的,看这里:coliru.stacked-crooked.com/a/1bce1b3c7e5d0237。我用名为my_forward 的实现更改了forward,它仅适用于 左值。您的代码仍然可以编译。但是,如果您查看declaration of std::forward,您会看到有 2 个重载:(1) 和 (2)。 (2) 取prvalue。我的问题是为什么(2)是必要的,什么是用例?如您所见,您的代码适用于仅为左值定义的my_forward。我问的有意义吗?
  • 在您的情况下,forward&lt;vector&lt;int&gt;&gt;(v)std::move(v) 相同(请参阅声明和参考折叠规则)。 v 本身仍然是一个左值(有一个名字)。
  • 啊事实上,我的代码使用了我想展示的第一个重载,你的意思是另一个重载。它是类似的,但是对于第二个重载,您接受显式移动的内容(如果您删除第一个重载,除非您使用 std::move 显式移动,否则代码将无法编译,删除引用会使第二次重载无法隐式移动仅)
  • 看这里,当仅与另一个重载一起移动时它不会编译 ideone.com/4Vvlnk 是的,你问的对我来说很有意义
【解决方案3】:

我之前盯着这个问题看,读了 Howard Hinnant 的链接,经过一个小时的思考后无法完全理解。现在我正在寻找并在五分钟内得到答案。 (编辑:得到的答案太慷慨了,因为 Hinnant 的链接有答案。我的意思是我理解,并且能够以更简单的方式解释它,希望有人会有所帮助)。

基本上,这允许您根据传入的类型在某些情况下通用。考虑以下代码:

#include <utility>
#include <vector>
#include <iostream>
using namespace std;

class GoodBye
{
  double b;
 public:
  GoodBye( double&& a):b(std::move(a)){ std::cerr << "move"; }
  GoodBye( const double& a):b(a){ std::cerr << "copy"; }
};

struct Hello {
  double m_x;

  double & get()  { return m_x; }
};

int main()
{
  Hello h;
  GoodBye a(std::forward<double>(std::move(h).get()));
  return 0;
}

此代码打印“移动”。有趣的是,如果我删除std::forward,它会打印副本。对我来说,这很难让我想到,但让我们接受它并继续前进。 (编辑:我想这是因为 get 将返回对右值的左值引用。这样的实体衰减为左值,但 std::forward 会将其转换为右值,就像 forward 的常见用法一样。仍然感觉不直观不过)。

现在,让我们想象另一个类:

struct Hello2 {
  double m_x;

  double & get() & { return m_x; }
  double && get() && { return std::move(m_x); }
};

假设main中的代码,h是Hello2的一个实例。现在,我们不再需要 std::forward,因为对 std::move(h).get() 的调用返回了一个右值。但是,假设代码是通用的:

template <class T>
void func(T && h) {
  GoodBye a(std::forward<double>(std::forward<T>(h).get()));
}

现在当我们调用func 时,我们希望它能够与HelloHello2 一起正常工作,即我们希望触发一个移动。如果我们包含外部std::forward,那么只有右值Hello 才会发生这种情况,所以我们需要它。但是……我们说到了重点。当我们将一个右值Hello2 传递给这个函数时,get() 的右值重载将已经返回一个右值双精度值,所以std::forward 实际上是接受一个右值。因此,如果不这样做,您将无法像上面那样编写完全通用的代码。

该死的。

【讨论】:

  • 如果答案是你的工作,为什么你的代码与我的答案相似? (更不用说你在我的回答中包含&lt;vector&gt;,但你不使用它:/感觉非常复制粘贴和编辑,所以为什么不直接编辑呢?)..
  • 除了表面上没有相似之处。我最初将您的代码复制并粘贴到我的 ide 中是因为我很好奇。然后我想出了答案,并修改了我已经拥有的代码。我没有编辑您的答案,因为它不正确,修复它不会是一个小的修改。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-15
  • 1970-01-01
  • 2012-10-08
相关资源
最近更新 更多