C++ 新特性 | C++ 11 | 移动语义与右值引用

一、移动语义与右值引用

1、左值与左值引用

左值是一个表示数据的表达式,程序可以获取其地址。左值可以出现在赋值语句的左边,也可以出现在赋值语句的右边。左值引用就是对左值的引用,例如:

int a = 20;   // a 左值
const int b = 10;  // b 左值
int c = b;  // c 左值
int &r = a;  // r 左值引用

左值一般有下面这些,如下:

  • 函数名和变量名
  • 返回左值引用的函数调用
  • 前置自增自减表达式++i、–i
  • 由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)
  • 解引用表达式*p

2、右值与右值引用

右值即可出现在赋值表达式右边,但不能获取其地址。右值包括字面常量(C风格字符串除外,它表示的是地址)、x + y表达式、以及返回值的函数(条件是该函数返回的不是引用)。C++11新增了右值引用,这是使用&&表示的,右值引用就是对右值的引用,例如:

int getValue(){
    return 50;
}
 
int main(){
    int x = 10;
    int y = 20;
    int &&r1 = 30;  //字面常量是右值
    int &&r2 = x + y;  //表达式是右值
    int &&r3 = getValue();  //函数返回的是int类型的值
    return 0;
}

右值一般有下面这些,如下:

  • 除字符串字面值外的字面值
  • 返回非引用类型的函数调用
  • 后置自增自减表达式i++、i–
  • 算术表达式(a+b, a*b, a&&b, a==b等)
  • 取地址表达式等(&a)

注意:右值引用本身是左值,右值引用本身有名字且可以取地址

3、将亡值

将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。例如:


class A {
    xxx;
};
A a;
auto c = std::move(a); // c是将亡值
auto d = static_cast<A&&>(a); // d是将亡值

4、移动语义

讲移动语义之前一定要先搞清楚浅拷贝深拷贝。移动语义,可以理解为转移所有权,深拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,就是转移资源的所有权,通过C++11新增的移动语义可以省去很多拷贝负担。下面实现一个自定义字符串类MyString,MyString内部管理一个C语言的char *数组,这个时候一般都需要实现拷贝构造函数和拷贝赋值函数,实现深拷贝,例如:

#include 
#include 
#include 
using namespace std;

class MyString
{
public:
    static size_t CCtor; //统计调用拷贝构造函数的次数

public:
    // 构造函数
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
   }

   // 拷贝构造函数
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }

   // 赋值运算符
   MyString& operator=(const MyString& str){
       if (this == &str)
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }

   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};

size_t MyString::CCtor = 0;

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000);
    for(int i = 0; i < 1000; i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << MyString::CCtor << endl;
}

输出结果:

1000

Process returned 0 (0x0)   execution time : 0.052 s
Press any key to continue.

代码看起来挺不错,却发现执行了1000次拷贝构造函数,如果MyString(“hello”)构造出来的字符串本来就很长,构造一遍就很耗时了,最后却还要拷贝一遍,而MyString(“hello”)只是临时对象,拷贝完就没什么用了,这就造成了没有意义的资源申请和释放操作,如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间,而C++11新增加的移动语义就能够做到这一点。要实现移动语义就必须增加两个函数:移动构造函数移动赋值运算符

#include 
#include 
#include 
using namespace std;

class MyString
{
public:
    static size_t CCtor; //统计调用拷贝构造函数的次数
    static size_t MCtor; //统计调用移动构造函数的次数
    static size_t CAsgn; //统计调用拷贝赋值函数的次数
    static size_t MAsgn; //统计调用移动赋值函数的次数

public:
    // 构造函数
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
   }

   // 拷贝构造函数
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }

   // 移动构造函数
   MyString(MyString&& str) noexcept
       :m_data(str.m_data) {
       MCtor ++;
       str.m_data = nullptr;
   }

   // 重载拷贝赋值运算符
   MyString& operator=(const MyString& str){
       CAsgn ++;
       if (this == &str)
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   // 重载移动拷贝赋值运算符
   MyString& operator=(MyString&& str) noexcept{
       MAsgn ++;
       if (this == &str)
          return *this;

       delete[] m_data;
       m_data = str.m_data;
       str.m_data = nullptr;
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }

   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};

size_t MyString::CCtor = 0;
size_t MyString::MCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MAsgn = 0;

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000);
    for(int i = 0; i < 1000; i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;
}

输出结果:

CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0

Process returned 0 (0x0)   execution time : 0.268 s
Press any key to continue.

可以看到,移动构造函数拷贝构造函数的区别是,拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数是MyString&& str,是右值引用,而MyString(“hello”)是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,通过移动构造函数实现了资源转移。

使用过程要注意的问题:

  • 如果没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因
  • c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝

5、std::move

前面已经看到使用std::move是为了实现移动语义,下面看下std::move的实现原理,源码如下:

/**
 *  @brief  Convert a value to an rvalue.
 *  @param  __t  A thing of arbitrary type.
 *  @return The parameter cast to an rvalue-reference to allow moving it.
*/
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type&&>(t);
}

remove_reference定义

/// remove_reference
template<typename _Tp>
  struct remove_reference
  { typedef _Tp   type; };

template<typename _Tp>
  struct remove_reference<_Tp&>
  { typedef _Tp   type; };

template<typename _Tp>
  struct remove_reference<_Tp&&>
  { typedef _Tp   type; };

std::remove_reference 结构体的实现非常简单,功能就是依靠模板把传参 _Tp 的类型分离出来,当调用 std::remove_reference::type 时即为分离出的最底层类型,测试代码如下:

#include 
#include 
using namespace std;

int main() {
    int i = 10;
    std::remove_reference<decltype(200)>::type a = 20;
    std::remove_reference<decltype((i))>::type b = 30;
    std::remove_reference<decltype(std::move(i))>::type c = 40;

    std::cout << std::is_same<int, std::remove_reference<decltype(200)>::type>::value << std::endl;
    std::cout << std::is_same<int, std::remove_reference<decltype((i))>::type>::value << std::endl;
    std::cout << std::is_same<int, std::remove_reference<decltype(std::move(i))>::type>::value << std::endl;

    return 0;
}

输出结果:

1
1
1

Process returned 0 (0x0)   execution time : 0.069 s
Press any key to continue.

说明 std::remove_reference 可以很好的将类型提取出来,即 int& 和 int&& 都可以提取出基础类型 int

某种意义上来说,std::move(lvalue) 就约等于 static_cast(lvalue),即将左值强制转换为右值。而 std::move 中封装了一个类型提取器 std::remove_reference 来方便使用。

6、universal references(通用引用)

当右值引用和模板结合的时候,就复杂了。T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。例如:

template<typename T>
void f( T&& param){
    ...
}
f(10);  // 10是右值
int x = 10;
f(x);  // x是左值

如果上面的函数模板表示的是右值引用的话,肯定是不能传递左值的,但是事实却是可以。这里的&&是一个未定义的引用类型,称为universal references,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。

注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references,例如:

template<typename T>
void f( T&& param); // 这里T的类型需要推导,所以&&是一个 universal references

template<typename T>
class Test {
	Test(Test&& rhs);  // Test是一个特定的类型,不需要类型推导,所以&&表示右值引用  
};

7、std::move中的引用折叠

std::move的形参类型为 _Tp&& ,如下:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type&&>(t);
}

由于形参类型是_Tp&&,当_Tp的实际类型是string&,此时的类型 string& && 又是什么?此处便涉及到了引用折叠。简单来说就是除了右值的&&是右值,其他都是左值

  • X& &、X&& &、X& && —— 折叠成X&,用于处理左值
  • X&& && —— 折叠成X&&,用于处理右值

引用折叠的意义就是让参数可以与任何类型的实参匹配,简单说就是右值传进来还是右值,左值传进来还是左值。 std::forward实现完美转发也是很大程度依赖于引用折叠这个东西。

8、std::forward实现完美转发

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。例如:下面是一个没有实现完美转发的例子

#include 
#include 
#include 
using namespace std;

void RunCode(int &val) {
    cout << "lvalue ref, val = " << val << endl;
}

void RunCode(int &&val) {
    cout << "rvalue ref, val = " << val << endl;
}

void perfectForward(int && val) {
    RunCode(val);
}

int main()
{
    int a = 0;
    RunCode(a);        // a被视为左值, RunCode(int &)
    RunCode(1);        // 1被视为左值, RunCode(int &&)
    RunCode(move(a));  // 强制将a由左值改为右值, RunCode(int &&)

    return 0;
}

输出结果:

lvalue ref, val = 0
rvalue ref, val = 1
rvalue ref, val = 0

Process returned 0 (0x0)   execution time : 0.321 s
Press any key to continue.

上面的例子就是不完美转发,而c++中提供了一个std::forward()模板函数解决这个问题。如下:

#include 
#include 
#include 
using namespace std;

void RunCode(int &m) {
    cout << "lvalue ref" << endl;
}

void RunCode(int &&m) {
    cout << "rvalue ref" << endl;
}

void RunCode(const int &m) {
    cout << "const lvalue ref" << endl;
}

void RunCode(const int &&m) {
    cout << "const rvalue ref" << endl;
}

// 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
template<typename T>
void perfectForward(T && t) {
    RunCode(forward<T> (t));
}

template<typename T>
void notPerfectForward(T && t) {
    RunCode(t);
}

int main()
{
    int a = 0;
    int b = 0;
    const int c = 0;
    const int d = 0;

    notPerfectForward(a);       // lvalue ref
    notPerfectForward(move(b)); // lvalue ref
    notPerfectForward(c);       // const lvalue ref
    notPerfectForward(move(d)); // const lvalue ref

    cout << endl;
    perfectForward(a);          // lvalue ref
    perfectForward(move(b));    // rvalue ref
    perfectForward(c);          // const lvalue ref
    perfectForward(move(d));    // const rvalue ref

    return 0;
}

输出结果:

lvalue ref
lvalue ref
const lvalue ref
const lvalue ref

lvalue ref
rvalue ref
const lvalue ref
const rvalue ref

Process returned 0 (0x0)   execution time : 0.062 s
Press any key to continue.

上面的代码结果表明,在universal referencesstd::forward的合作下,能够完美的转发这4种类型

9、std::forward实现原理

std::foward函数原型,如下:

  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
            " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

forward函数有两个重载版本:

  • 第一个,参数是左值引用,可以接受左值
  • 第二个,参数是右值引用,可以接受右值

根据引用折叠的原理,如果传递的是左值,Tp推断为string&,则返回变成static_cast,也就是static_cast,所以返回的是左值引用。如果传递的是右值,Tp推断为string或string&&,则返回变成static_cast,所以返回的是右值引用。反正不管怎么着,都是一个引用,那就都是别名,也就是谁读取std::forward,都直接可以得到std::foward所赋值的参数。这就是完美转发的基本原理!

你可能感兴趣的:(C++,新特性,c++,java,算法)