右值

左值和右值,最早是从 C 语言继承而来的。在 C 语言,或者继承版本,的解释中,

  • 左值是可以位于赋值运算符=左侧的表达式(当然,左值也可以位于=的右侧),而
  • 右值是不可以位于赋值运算符=左侧的表达式。

C++11 标准中引入的最强有力的特性就是右值引用,以及相关的移动语义 (move semantics)概念。
右值引用的意义通常解释为两大作用:移动语义和完美转发(perfect forwarding)。
移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。而在C++11之前,移动语义的缺失是C++饱受诟病的问题之一。

为了实现移动语义,首先需要解决的问题是,如何标识对象的资源是可以被移动的呢?这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。

右值(相对应的还有左值)是从C语言设计时就有的概念,但因为其如此基础,也是一个最常被忽略的概念。不严格的来说,左值对应变量的存储位置,而右值对应变量的值本身。C++中右值可以被赋值给左值或者绑定到引用。类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。

右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast()的简单封装。

函数实参(实际传的那个变量)和形参(括号里声明的那个变量)的适配性:
右值引用 (Rvalue Reference) - 图1

  1. #include <iostream>
  2. #include <string>
  3. int g_var = 8;
  4. int& returnALvalue() {
  5. return g_var; //here we return a left value
  6. }
  7. int returnARvalue() {
  8. return g_var; //here we return a r-value
  9. }
  10. void print(std::string& name) {
  11. std::cout << "lvalue detected: " << name << std::endl;
  12. }
  13. void print(const std::string& name) {
  14. std::cout << "const value detected: " << name << std::endl;
  15. }
  16. void print(std::string&& name) {
  17. std::cout << "rvalue detected: " << name << std::endl;
  18. }
  19. int main(int argc, char const* argv[]) {
  20. // test rvalue reference
  21. std::cout << returnARvalue() << std::endl;
  22. std::cout << returnALvalue()++ << std::endl;
  23. std::cout << returnARvalue() << std::endl;
  24. // must const
  25. const std::string& rname = "rvalue";
  26. // test lvalue and rvalue
  27. std::string name = "lvalue";
  28. const std::string cname = "cvalue";
  29. print(name);
  30. print(cname);
  31. print("rvalue");
  32. return 0;
  33. }

输出:

  1. 8
  2. 8
  3. 9
  4. lvalue detected: lvalue
  5. const value detected: cvalue
  6. rvalue detected: rvalue

一个表达式是左值还是右值,取决于我们使用的是它的值还是它在内存中的位置(作为对象的身份)。也就是说一个表达式具体是左值还是右值,要根据实际在语句中的含义来确定。例如:

  1. int foo(42);
  2. int bar;
  3. // 将 foo 的值赋给 bar,保存在 bar 对应的内存中
  4. // foo 在这里作为表达式是右值;bar 在这里作为表达式是左值
  5. // 但是 foo 作为对象,既可以充当左值又可以充当右值
  6. bar = foo;

因为 C++ 中的对象本身可以是一个表达式,所以这里有一个重要的原则,即

  • 在大多数情况下,需要右值的地方可以用左值来替代,但
  • 需要左值的地方,一定不能用右值来替代。

又有一个重要的特点,即

  • 左值存放在对象中,有持久的状态;而
  • 右值要么是字面常量,要么是在表达式求值过程中创建的临时对象,没有持久的状态。

这两个特性意味着:接受和使用右值引用的代码,可以自由地接管所引用的对象的资源,而无需担心对其他代码逻辑造成数据破坏。

Move 语义

  1. &&

完美转发

  1. template<typename T, typename ArgT>
  2. std::shared_ptr<T> factory(const ArgT& arg) {
  3. return shapred_ptr<T>(new T(arg));
  4. }

factory函数有两个模板参数T与ArgT,并假定类型T有一个构造函数,可以接受const ArgT&类型的参数,进行T类型对象的构造,然后返回一个T类型的智能指针,指向构造出来的对象。
毫无疑问,在这个例子里,factory函数的arg变量既可以接受左值,也可以接受右值(允许将右值绑定在常量左值引用上)。但这里还有一个问题,按照之前的分析,不论arg接受的是什么类型,到了factory函数内部,arg本身都将是一个左值。这样一来,假设类型T的构造函数支持对ArgT类型的右值引用,也将永远不会被调用。也就是说,factory函数无法实现 move 语义,也就无法不能算是完美转发。
这里我们引入一个函数,它是标准库的一部分:

  1. template<class S>
  2. S&& forward(typename std::remove_reference<S>::type& a) noexcept
  3. {
  4. return static_cast<S&&>(a);
  5. }

当a的类型是S&的时候,函数将返回S&;当a的类型是S&&的时候,函数将返回S&&。因此,在这种情况下,我们只需要稍微改动工厂函数的定义就可以了:

  1. template<typename T, typename ArgT>
  2. std::shared_ptr<T> factory(ArgT&& arg) {
  3. return std::shapred_ptr<T>(new T(std::forward<ArgT>(arg)));
  4. }

std::move

标准库还定义了std::move函数,它的作用就是将传入的参数以右值引用的方式返回。

  1. template<class T>
  2. typename std::remove_reference<T>::type&&
  3. std::move(T&& a) noexcept
  4. {
  5. typedef typename std::remove_reference<T>::type&& RvalRef;
  6. return static_cast<RvalRef>(a);
  7. }

移动迭代器(move_iterator)

现在假设有这样一个容器std::vector>,即在向量中保存了若干指向RegTree的unique_ptr智能指针;又有一个函数BoostNewTrees(std::vector>& ret),将会首先清洗ret中的数据,然后再将新的数据放入ret中。
现在,我需要循环多次执行BoostNewTrees函数,并将他们生成的数据依次放入一个容器里。那么,下面的代码会产生编译错误:

  1. std::vector<std::unique_ptr<RegTree>> ret;
  2. for (size_t i(0); i != limit; ++i) {
  3. std::vector<std::unique_ptr<RegTree>> tmp;
  4. BoostNewTrees(tmp);
  5. ret.insert(ret.end(), tmp.begin(), tmp.end()); // compile error!
  6. }

这是因为,在调用ret.insert()函数时,传入的迭代器tmp.begin()在解引用时,会返回std::unique_ptr&,进而尝试调用拷贝构造函数unique_ptr(const unique_ptr&),复制内容。然而,该函数被声明为「删除的」,不允许用户调用,于是报错。
为此,我们需要调用std::make_move_iterator函数(定义在iterator头文件里),将普通的迭代器转换为移动迭代器。相比普通迭代器,移动迭代器仅仅在解引用时的行为有不同:它将返回元素类型的右值引用(而不是普通迭代器返回的左值引用)。这相当于对普通迭代器每次解引用之后,都调用一次std::move获取右值引用。于是,在进行insert的时候,调用的就是移动构造函数unique_ptr(unique_ptr&&)了,而这是允许的。

  1. std::vector<std::unique_ptr<RegTree>> ret;
  2. for (size_t i(0); i != limit; ++i) {
  3. std::vector<std::unique_ptr<RegTree>> tmp;
  4. BoostNewTrees(tmp);
  5. ret.insert(ret.end(),
  6. std::make_move_iterator(tmp.begin()),
  7. std::make_move_iterator(tmp.end()));
  8. }
  9. // now we get `ret`.

What is the difference of between std::move and std::forward

std::move takes an object and allows you to treat it as a temporary (an rvalue). Although it isn’t a semantic requirement, typically a function accepting a reference to an rvalue will invalidate it. When you seestd::move, it indicates that the value of the object should not be used afterwards, but you can still assign a new value and continue using it.

std::forward has a single use case: to cast a templated function parameter (inside the function) to the value category (lvalue or rvalue) the caller used to pass it. This allows rvalue arguments to be passed on as rvalues, and lvalues to be passed on as lvalues, a scheme called “perfect forwarding.”

  1. std::map<std::string, std::function<void()>> commands;
  2. template<typename ftor>
  3. void install_command(std::string name, ftor && handler)
  4. {
  5. commands.insert({
  6. std::move(name),
  7. std::forward<ftor>(handler)
  8. });
  9. }

Details in: http://bajamircea.github.io/coding/cpp/2016/04/07/move-forward.html

Value Category

image.png

Details in: http://bajamircea.github.io/coding/cpp/2016/04/07/move-forward.html
Value Category details in: https://en.cppreference.com/w/cpp/language/value_category

参考

  1. 理解C++中的左值和右值
  2. C++ 中的右值引用
  3. 如何理解 C++ 的右值引用
  4. Reference declaration
  5. std::forward