C++11 的右值引用问题

C++11 中增加了一个新的特性,叫做“右值引用” (rvalue reference)。

主要参考文献

Abstract

Rvalue references is a small technical extension to the C++ language. Rvalue references allow programmers to avoid logically unnecessary copying and to provide perfect forwarding functions. They are primarily meant to aid in the design of higer performance and more robust libraries.

注解

  1. 为了避免无谓拷贝
  2. 为了提供完美转寄函数
  3. 主要目的是为了写通用库。

个人以为,一般应用程序的作者,尽量别用这个功能。

怎么理解这个功能呢?

首先理解什么是左值,右值。十分困难给出一个严格的定义,个人觉得一下非正 式定义比较容易理解。

  1. 能取地址的表达式,就是左值。
  2. 不是左值的表达式,就是右值。

最常见的右值

  1. 常量表达式 1 + 2,
  2. 函数调用参数的临时对象 f(A_Class())中 f 的实参。
  3. 函数调用的返回值产生的临时变量。

最简单的一个例子

#include <iostream>
using namespace std;
void foo(int && a)
{
    cout << __PRETTY_FUNCTION__ << ' ' << a << endl;
}
void foo(int& a)
{
    cout << __PRETTY_FUNCTION__ << ' ' << a << endl;
}
int bar()
{
    return 1000;
}
int main(int argc, char *argv[])
{
    int i = 100;
    foo(10);
    foo(i);
    foo(bar());
    return 0;
}

程序输出结果

% clang++ -ggdb -O0 -Wall -Werror -std=c++11 rvalue0.cpp && ./a.out
void foo(int &&) 10
void foo(int &) 100
void foo(int &&) 1000

普通函数调用重载理解了,就不难理解转移构造函数了。

谨慎使用这个功能

C++ 的编译器已经十分聪明,在一般情况下,已经尽最大可能避免无谓的对象拷 贝。例如

#include <iostream>
using namespace std;
struct A {
    A();
    ~A();
    A(const A& a);
    int value;
};
A::A()
{
    cout << __PRETTY_FUNCTION__ << " " << (void*) this << endl;
}
A::~A()
{
    cout << __PRETTY_FUNCTION__ << " " << (void*) this << endl;
}

A::A(const A& a)
{
    cout << __PRETTY_FUNCTION__ << " " << (void*) this << endl;
}
A bar()
{
    A ret;
    cout << __PRETTY_FUNCTION__ << " " << (void*) &ret << endl;
    return ret;
}
void foo(A x)
{
    cout << __PRETTY_FUNCTION__ << (void*)&x <<endl;
}
int main(int argc, char *argv[])
{
    foo(bar());
    return 0;
}

结果如下

% clang++ -ggdb -O0 -Wall -Werror -std=c++11 rvalue0.cpp && ./a.out
A::A() 0x7ffff687a968
A bar()
 0x7ffff687a968
void foo(A)0x7ffff687a968
A::~A() 0x7ffff687a968

左值引用是右值

void foo(A && x){
    ...
    x
}

在函数体中,x 不是右值,而是左值,因为可以取到 x 的地址。

std::move 的作用

这函数就是一个强制类型转换,把左值转换为左值,没有任何函数开销。如果没 有这个函数,一般很难调用转移构造函数(move constructor)。

可否返回一个临时对象的右值引用?

A&& foo()
{
     A x;
     return std::move(A)
     // or return x;
}

无论怎样,和返回普通引用一样,导致程序崩溃。

为什么有这个特性呢?

  1. 实现“可转移“ , 和 std::move 相关
  2. 提供完美转寄函数,和 std::forward 相关。(perfect forwarding function 怎么译?)

可转移

有很多时候,对象是可移动的而是不可以拷贝的。上面参考文献给出三个例子

  1. fstream
  2. unique_ptr (这里)[{{site.baseurl}}/{% post_url 2015-07-08-C-------feature--unique-ptr %}
  3. 线程对象

如果拷贝构造函数是私有函数,那么就是被禁止的。C++11 中有新特性,可以定 义 delete 函数。

一般来讲,一个对象拥有某一资源,例如内存,文件,线程等等,这种所有权是 不能与其他对象共享的,因为析构函数会释放资源,如果和别人共享,”别人“被 析构了,析构函数会释放资源,那么你手里就拿着一个被释放的资源。类似这样 的对象就是不可以拷贝的

但是_不可拷贝对象_并不是_不可转移对象_,假如拷贝了一个对象之后,但是源 对象放弃了所有权,那么目标对象就可以拥有所有权了。这个时候,我们不叫做 拷贝(copy),叫做转移(move)。

ifstream foo()
{
    ifstream fs(__FILE__);
    return fs;
}

这个例子会有编译错误,因为 ifstream 没有可用的拷贝构造函数,即“不可拷 贝”。但是在新的标准里面,解决了这个问题。 可惜的是 GCC-4.8 还没有实现 这个 ifstream 的转移构造函数。为了凑数,下面用 unique_ptr 为例。

#include <iostream>
#include <fstream>
#include <memory>
#include <iterator>
using namespace std;
unique_ptr<ifstream> foo()
{
    return std::move(unique_ptr<ifstream>(new ifstream(__FILE__)));
}
int main(int argc, char *argv[])
{
    unique_ptr<ifstream> fs = foo();
    copy(istreambuf_iterator<char>(*fs),istreambuf_iterator<char>(),
         ostream_iterator<char>(cout));
    return 0;
}

完美转寄函数(Perfect Forward function)

假设我们写一个函数 g(a1) ,就和调用 f(a1) 一模一样。很容易吗?

#include <iostream>
#include <vector>
using namespace std;
class A {
public:
    explicit A(int v);
    A(const A& other);
    ~A();
public:
    int value;
};
A::A(int v):value(v)
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
A::A(const A& other):value(other.value)
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::~A()
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
void f(A a)
{
    cout << a.value << endl;
}
template<class T>
void g(T a)
{
    f(a);
}

int main(int argc, char *argv[])
{
    A x(100);
    g<A>(x);
    return 0;
}

程序输出

A::A(int) 100 0x7fff48d239e8
A::A(const A &) 100 0x7fff48d239e0 0x7fff48d239e8
A::A(const A &) 100 0x7fff48d239a8 0x7fff48d239e0
100
A::~A() 100 0x7fff48d239a8
A::~A() 100 0x7fff48d239e0
A::~A() 100 0x7fff48d239e8

可以明显看到有一次多余拷贝构造函数。容易,改成引用就行了。

template<class T>
void g(T& a)
{
    f(a);
}

这样的确避免了这次多余的拷贝构造函数,但是就没有那么通用了。所以我的体 会是,这个 rvalue reference 的功能是给库作者用的,普通应用程序,不用搞 得这么复杂。

不通用的原因是,如果传递常量引用,就会编译错误。

const A x(100);
g<A>(x);

你会说,容易改,弄一个函数重载就行了。

template<class T>
void g(T& a)
{
    f(a);
}
template<class T>
void g(const T& a)
{
    f(a);
}

搞定。

这样是搞定了,但是如果这样的函数多了,写起来十分难看,可读性也很差。 c++11 rvalue reference 这样解决了这个问题。

#include <iostream>
#include <vector>
using namespace std;
class A {
public:
    explicit A(int v);
    A(const A& other);
    A(A&& other);
    ~A();
public:
    int value;
};
A::A(int v):value(v)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
A::A(const A& other):value(other.value)
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::A(A&& other):value(other.value)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::~A()
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
void f(A a)
{
    cout << a.value << endl;
}
template<class T>
void g(const T&& a)
{
    f(a);
}

int main(int argc, char *argv[])
{
    A x(100);
    g<A>(std::move(x));
    return 0;
}

写一个通用的完美转寄函数十分困难,标准已经弄好了。std::forward

如何使用 std::forward

参考 (std::forward)[http://en.cppreference.com/w/cpp/utility/forward]

template<class T>
void wrapper(T&& arg)
{
    foo(std::forward<T>(arg)); // Forward a single argument.
}
  1. 如果调用 wrapper 的实参是 std::string 类型的右值引用,那么 T 是 std::string (不是 std::string& 也不是 const std::string& 也 不是 std::string&&) ,那么 std::forward 保证传递右值引用给 foo 函数,即调用重载右值引用的 foo
  2. 如果调用 wrapper 的实参是 左值常量的 std::string ,那么 Tconst std::string& 并且 std::forward 保证传递常量左值引用给 foo 函数,即调用重载常量左值引用的 foo
  3. 如果调用 wrapper 的实参是非常量左值 std::string,那么 Tstd::string& ,并且 std::forward 保证传递非常量左值给 foo , 即调用重载非常量左值的 foo

听上去比较复杂,简单理解就是,怎么调用 wrapper ,就怎么调用 foo , 不用写很多的重载函数,只用一个 wrapper 函数,利用 std::forward , 就可以找到合适版本的 foo,就算 foo 有很多个重载版本。

#include <iostream>
#include <vector>
#include <string>
using namespace std;
void foo(std::string&& a)
{
    cout << __PRETTY_FUNCTION__ << " " <<  a << endl;
}
void foo(const std::string& a)
{
    cout << __PRETTY_FUNCTION__ << " " <<  a << endl;
}
void foo(string& a)
{
    cout << __PRETTY_FUNCTION__ << " " <<  a << endl;
}
template<class T>
void wrapper(T&& arg)
{
    foo(std::forward<T>(arg)); // Forward a single argument.
}

int main(int argc, char *argv[])
{
    string a = "lvalue";
    const string b = "const lvalue";
    wrapper(string("rvalue"));
    wrapper(b);
    wrapper(a);

    return 0;
}

输出结果

% lang++ -ggdb -O0 -Wall -Werror -std=c++11 forward3.cpp && ./a.out
void foo(std::string &&) rvalue
void foo(const std::string &) const lvalue
void foo(string &) lvalue

别重新发明轮子了,标准库已经做好了,而且这个工作并不简单。

不要用右值引用代替返回值优化

前面说了 C++11 rvalue reference 的两个用处。经常看到有人说,还有一个用 处,就是,如果一个函数返回一个临时变量, rvalue reference 可以提高性能。 我觉得这是一个误解, 返回值优化 (RVO) 已经是十分成熟的技术,不需要 rvalue reference ,有了这个,反而更慢。前面的例子已经看到,C++ 编译器 足够聪明,很多情况下已经避免了无谓拷贝。

#include <iostream>
#include <vector>
using namespace std;
class A {
public:
    explicit A(int v);
    A(const A& other);
    A(A&& other);
    ~A();
public:
    int value;
};
A::A(int v):value(v)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
A::A(const A& other):value(other.value)
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::A(A&& other):value(other.value)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::~A()
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
A f()
{
    return A(100);
}
int main(int argc, char *argv[])
{
    A x = f();
    return 0;
}

这个例子中

% clang++ -ggdb -O0 -Wall -Werror -std=c++11 rvo.cpp && ./a.out
A 100 0x7fff76a18a78
~A 100 0x7fff76a18a78

可以看到,没有任何多多余动作,RVO 工作的很好。我们画蛇添足一下,用 rvalue reference 看看。

改成

A && x = f()

程序输出没有任何变化。

如果

return A(100) => return std::move(A(100))

反倒多了一次转移构造函数的调用和临时对象的析构。

总结

  1. 右值引用是为了库函数作者用的,尤其是模板库。普通应用程序尽量不要用, 除非你真正理解这个功能。
  2. std::move 经常和转右值构造函数配合使用,转移构造函数的形参是右值引用。
  3. std::forward 利用右值引用。尽量用这个。