c++11 右值、右值引用、移动语义、完美转发必须搞清楚

引子

在介绍题目中内容之前我们先看一个几个示例

引子1-起源

作为一种追求执行效率的语言, C++在用临时对象或函数返回值给左值对象赋值时的深度拷贝(deep copy)一直受到诟病。 考虑到临时对象的生命期仅在表达式中持续,如果把临时对象的内容直接移动(move)给被赋值的左值对象,效率改善将是显著的。这就是移动语义的来源。

示例1

#include <iostream>
#include <memory>
using namespace std;
struct Base
{
  	Base():d(new int(0)){}   
    ~Base(){delete d;}
    int *d;
};
int main()
{
	Base a;
  Base b(a);
  cout<<*a.d<<endl;
  cout<<*b.d<<endl;
	return 0;
}

g++ dbFreeTest.c -std=c++11 -o dbfree
运行会出先如下错误

0
析构
0
析构
free(): double free detected in tcache 2
Aborted

分析原因为:b(a)会调用合成的拷贝构造函数,而此处调用后是浅拷贝,只是使得b的d指针指向了a的d指针所指向的内存,并没拷贝数据;而释放时先释放掉a,a的d所指向的内存释放掉,此时b的d就成了悬挂指针,指向的内存已不复存在,再次释放就会导致double free

示例2(改进的示例1)

针对示例1,我们可以自定义拷贝构造函数
改造如下

struct Base
{
  	Base():d(new int(0)){}   
  	Base(Base & h):d(new int(*h.d)){}//拷贝构造函数,从堆中分配内存,并用*h.d进行初始化

    ~Base(){delete d;}
    int *d;
};

运行后不存在两次释放的问题了

0
0
析构
析构

那么a.d所指的内存很大的时,这样拷贝的效率将会变的非常低

示例3

#include <iostream>
#include <memory>

using namespace std;

struct Base
{
  	Base():d(new int(0)){}
    Base(const Base&)=delete;
    Base(Base&& h)noexcept:d(h.d)
    {
        h.d = nullptr;
        cout << "Move constructor: dynamic array is moved!\n";
    }
    ~Base(){cout<<"析构"<<endl;delete d;}
    int *d;
};
int main()
{
	Base a;
    cout<<*a.d<<endl;
  	Base b(std::move(a));
    cout<<*b.d<<endl;
	return 0;
}

运行结果:

0
Move constructor: dynamic array is moved!
0
析构
析构

这样我们就可以在构造b时不分配内存构造了。

引子2-性能

与传统的拷贝赋值运算符(copy assignment)成员函数、拷贝构造(copy ctor)成员函数对应,移动语义需要有移动赋值(move assignment)成员函数、移动构造(move ctor)成员函数的实现机制。可以通过函数重载来确定是调用拷贝语义还是移动语义的实现。
再看下述两个交换的例子,来理解下移动语义的好处:

示例1

template<class T>
void swap(T& a, T& b) 
{ 
  T tmp(a);
  a = b; 
  b = tmp; 
} 

X a, b;
swap(a, b);

示例2

template<class T> 
void swap(T& a, T& b) 
{ 
  T tmp(std::move(a));
  a = std::move(b); 
  b = std::move(tmp);
} 

X a, b;
swap(a, b);

总结

尽可能使用 std::move,如图所示 swap上面的函数,给了我们 以下重要好处:

  1. 对于那些实现移动语义的类型,许多标准算法和操作 将使用移动语义,从而体验到潜在的显着性能提升。 一个重要的例子是就地排序:就地排序算法几乎不做任何其他事情 但是交换元素,这种交换现在将利用所有类型的移动语义 提供它。
  2. STL 通常需要某些类型的可复制性,例如可以用作容器的类型 元素。 经过仔细检查,事实证明,在许多情况下,可移动性就足够了。 所以, 我们现在可以使用可移动但不可复制的类型( unique_pointer想到) 在许多以前不允许的地方。 例如,现在可以使用这些类型 作为 STL 容器元素。

什么是值?

下文出处
C++11标准引入了右值引用数据类型与移动语义,因而左值与右值的定义发生了很大变化。右值引用变量绑定到右值上,延长了右值对应的临时对象的生存期。移动语义把临时对象的内容移动(move)到左值对象上。
因而在C++11,对于值的分类,要考虑标识(identity)与可移动性(movability),二者的组合产生了五种分类:

左值lvalue

可以用取地址运算符&获取地址的表达式。也可定义为非临时对象或非成员函数。具有标识,但不可移动。这也是C++03的经典左值。可用于初始化左值引用。可以有不完备类型(incomplete type)。包括:

  1. 作用域中的变量名与函数名,不论其类型。因此,具名的右值引用,即具有右值引用类型的变量,也是左值表达式,这符合一般规律,不是特例
void foo(X&& x)
{
  X anotherX = x; // 调用 X(X const & rhs)拷贝构造,因为这是x一个具名的右值引用,所以是左值
}
X&& goo();
X x = goo(); //调用 X(X&& rhs)移动的拷贝构造 ,以为这是个不具名的右值引用,故为右值
  1. 函数调用表达式或重载运算符表达式,如果其返回类型为左值引用或者是到函数类型的右值引用。
  2. 内建的先增(前缀++)、先减(前缀–)、解引用(dereference)、赋值、复合赋值、下标(除了数组临终值)、成员访问(除了临终值的非静态非引用成员、成员枚举值、非静态成员函数),通过数据成员指针的访问且左端操作数为左值、逗号运算符且右端的操作数为左值、三元条件运算符(ternary conditional)且第二与第三操作数为左值。
  3. 到左值引用类型的类型转换表达式。
  4. 字符串字面量(string literal)
  5. 类型转换表达式,转换为到函数的右值引用

临终值xvalue(expiring value

具有标识,并且可以移动。对应的对象接近生存期结束,但其内容尚未被移走。可以多态;非类对象可以cv限定。包括:

  1. 函数调用或重载的运算符表达式,如果返回类型是到对象的右值引用。
  2. 类型转换表达式,转换为右值引用,如static_cast<T&&>(val)或(T&&)val
  3. 访问xvalue的非静态类成员。
  4. 指向数据成员的指针表达式,第一操作数是xvalue

纯右值prvalue

不具有标识,但可以移动。对应临时对象或不对应任何对象的值。纯右值不能是多态的;临时对象的动态类型是表达式类型;非类且非数组的纯右值不能是const限定的;不能有不完备类型(除了void)。包括:

  1. 字面量(除了字符串字面量)。
  2. 函数调用或重载的运算符表达式,如果返回类型不是引用。
  3. 内建后增、后减、算术与逻辑运算符、比较运算符、取地址运算符、访问成员枚举值、访问非静态成员函数、访问右值的非静态非引用数据成员、访问右值的数据成员指针或非静态函数成员指针、逗号运算符且右端操作数为右值、三元条件运算符且第二或第三操作数不是左值。
  4. 类型转换表达式,转换为非引用类型。
  5. Lambda表达式

广义左值glvalue

具有标识。包括左值与临终值。可以多态、动态类型。

右值rvalue

可以移动。包括濒死值与纯右值。不能通过&运算符取地址。

C++的非静态成员函数调用表达式(obj.func与ptr->func),非静态成员函数指针调用表达式(obj.*mfp与ptr->*mfp)被当作纯右值,但是不能用于初始化引用,不能做函数实参,仅仅能用作函数调用表达式左边的操作数,如(pobj->*ptr)(args)。

返回void的函数调用表达式、到void的类型转换表达式、throw表达式被当作纯右值。但是不能用于初始化引用,不能做函数实参。可用于某些上下文环境中(如单独作为一行语句、逗号操作符的左端表达式等),或返回void的函数的return语句中。此外,throw表达式可用作三元条件操作符的第二或第三操作数。

位元栏(bit field)表达式是左值,但不能用&运算符取地址,不能绑定到非常量左值引用。常量左值引用可以用位域左值初始化,但实际上是另行分配绑定了一个对象。
示例1:

  	int a = 42;
  	int b = 43;

  // a和b都是左值lvalues:
  	a = b; // ok
  	b = a; // ok
  	a = a * b; // ok

 // a * b 是一个右值 rvalue:
  	int c = a * b; // ok, 右值rvalue在表达式的右侧
  	a * b = 42; // error, 右值rvalue在表达式左侧

	int var = 0;
	var = 1 + 2; // ok, var在这是左值
	var + 1 = 2 + 3; // error, var + 1 是右值
	int* p1 = &var; // ok, var是左值可取址
	int* p2 = &(var + 1); // error,var + 1 是右值
	UserType().member_function(); // ok, calling a member function of the class rvalue

示例2:

  // 左值lvalues:
  //
  int i = 42;
  i = 43; // ok, i是一个左值lvalue
  int* p = &i; // ok, i 是一个左值 lvalue
  int& foo();
  foo() = 42; // ok, foo()是一个左值 lvalue
  int* p1 = &foo(); // ok, foo() 是一个左值 lvalue

  // 右值rvalues:
  //
  int foobar();
  int j = 0;
  j = foobar(); // ok, foobar() 是一个右值rvalue
  int* p2 = &foobar(); // error, 不能对一个右值进行取址
  j = 42; // ok, 42是一个右值rvalue

示例3

  • 下标操作符是这种形式的函数 T& operator[](T*, ptrdiff_t) ,所以A[0]是一个左值 ,其中A 是数组的类型。

  • 解引用操作符是这种形式的函数T& operator*(T*),因此*p 是一个左值, 其中 p 是一个指针类型
    运算符就是这种形式

  • 减运算符 T operator-(T) ,所以 -x 是一个右值 rvalue.

示例4

#include <iostream>
#include <utility>

int i = 101, j = 101;

int foo(){ return i; }
int&& bar(){ return std::move(i); }
void set(int&& k)
{ //不会发生折叠
	k = 102; //K 是个左值,具名的右值引用被当作左值
}
int main()
{
	foo();
	std::cout << i << std::endl;//101
	set(bar());
	std::cout << i << std::endl;//102
}

什么是声明符?

图片连接
大致需要知道的是我们使用的* 、&& 、&、[]其实都是声明符中的一种
声明符

关于引用

知道了值和声明符,那么就开始进行引用的说明吧。
在此之前再插入一条灵魂疑问:引用为何必须初始化?
如果引用未初始化,则无法对其进行初始化,因为任何分配给引用的尝试总是分配给它的所指对象。

int& numberRef;     // 假装这是允许的
numberRef = number; // 将数字复制到某个随机的存储位置

参考1:[A Brief Introduction to Rvalue References]
好了,接下来看下C++11引用绑定规则:

  • 非常量左值引用(X& ):只能绑定到X类型的左值对象;
  • 常量左值引用(const X&):可以绑定到 X、const X类型的左值对象,或X、const X类型的右值;[注 8]
  • 非常量右值引用(X&&):只能绑定到X类型的右值;
  • 常量右值引用(const X&&):可以绑定规定到X、const X类型的右值。
    由于右值引用主要针对移动语义用来修改被引用的对象的内容,所以常量右值引用(const X&&)较少用到。

注意事项:
1.引用必须被初始化为指代一个有效的对象或函数
2. 引用一旦初始化,便无法引用另一对象。
3. 因为引用不是对象,所以不存在引用的数组,不存在指向引用的指针,不存在引用的引用

int& a[3]; // 错误
int&* p;   // 错误
int& &r;   // 错误
  1. 不存在 void 的引用
  2. 用于声明引用的const都是底层const(底层const是指 指针所指的对象是一个常量;顶层const是指 指针本身是个常量),即引用类型无法在顶层被 cv 限定;声明中没有为此而设的语法,如果在typedef 名、 decltype 说明符或类型模板形参上添加了该限定符,它将被忽略。
	std::string str = "Test";
    std::string const &r3 = s;//OK:底层const
    const std::string  &r4 = s;//OK:底层const
    std::string  & const  r5 = s;//错误,
    //下面这两个均错误,无法修改指向的值
    r3 +="apple"; r4 +="apple";

左值引用

左值引用 (l-ref, lvalue reference) 用 & 符号引用 左值(但不能引用右值)。可拥有不同的 cv 限定。

当函数的返回值是左值引用时,函数调用表达式变成左值表达式

#include <iostream>
#include <string>
 
char& char_number(std::string& s, std::size_t n) {
    return s.at(n); // string::at() 返回 char 的引用
}
 
int main() {
    std::string str = "Test";
    char_number(str, 1) = 'a'; // 函数调用是左值,可被赋值
    std::cout << str << '\n';
}

示例:

struct A {};
struct B : A {} b;
 
A& ra = b;             // ra 引用 b 中的 A 子对象
const A& rca = b;      // rca 引用 b 中的 A 子对象

右值引用

右值引用(rvalue reference),是C++程序设计语言自C++11标准提出的一类数据类型。用于实现移动语义(move semantic)与完美转发(perfect forwarding)
无论是传统的左值引用还是C++11引进的右值引用,从编译后的反汇编层面上,都是对象的存储地址与自动解引用(dereference)。因此,右值引用与左值引用的变量都不能悬空(dangling),也即定义时必须初始化从而绑定到一个对象上。

设X为任何一种数据类型,则定义X&&是到数据类型X的右值引用(rvalue reference to X)。传统的引用类型X&被称为左值引用(lvalue reference to X)。
例如:

int i;
int &j=i; //定义传统的左值引用并初始化
int &&k=std::move(i);  //定义一个右值引用并初始化。std::move在<utility>中

引用折叠

通过模板或 typedef 中的类型操作可以构成引用的引用,此时适用引用折叠(reference collapsing)规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
注:decltype也可能会用到引用塌缩规则
对于一个类型X:

  • X& &、X& && 、X&& &都折叠成类型X&
  • 类型X&& &&折叠成X&&
    示例
typedef int&  lref;
typedef int&& rref;
int n;
//函数模板
template<typename T>void foo(T&&);
//typedef
lref&  r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref&  r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
//decltype
int var;
decltype(var)&& v1=std::move(var); //类型是int&&

== 注:引用折叠只能应用于间接创建的引用的引用,如类型别名或者模板参数 ==

转发引用

转发引用是一种特殊的引用,它保持函数实参的值类别,使得 std::forward 能用来转发实参。转发引用是下列之一:

  • 函数模板的函数形参,其被声明为同一函数模板的类型模板形参的无 cv 限定的右值引用
//示例1
template<class T>
int f(T&& x) {                    // x 是转发引用
    return g(std::forward<T>(x)); // 从而能被转发
}
int main() {
    int i;
    f(i); // 实参是左值,调用 f<int&>(int&), std::forward<int&>(x) 是左值
    f(0); // 实参是右值,调用 f<int>(int&&), std::forward<int>(x) 是右值
}
//示例2 
template<class T>
int g(const T&& x); // x 不是转发引用:const T 不是无 cv 限定的
//示例3
template<class T> struct A {
    template<class U>
    A(T&& x, U&& y, int* p); // x 不是转发引用:T 不是构造函数的类型模板形参
                             // 但 y 是转发引用
};
//示例4
template <class T > class vector {
    public: 
    void push_back(T&& x); // T是类模板参数 ⇒ 该成员函数不需要类型推导;这里的函数参数类型就是T的右值引用
     template <class Args> void emplace_back(Args&& args); //  该成员函数是个函数模板,有自己的模板参数,需要类型推导
};
//示例5
template<typename T>void f(const T&& param); // 这里的“&&”不需要类型推导,意味着“常量类型T的右值引用”
template<typename T>void f(std::vector<T>&& param);  // 这里的“&&”不需要类型推导,意味着std::vector<T>的右值引用
  • auto&&,但当其从花括号包围的初始化器列表推导时除外:
    • 如果初始化值(initializer)是类型A的左值,则声明的变量类型为左值引用A&;
    • 如果初始化值是类型A的右值,则声明的变量类型为右值引用A&&。
auto&& vec = foo();       // foo() 可以是左值或右值,vec 是转发引用
auto i = std::begin(vec); // 也可以
(*i)++;                   // 也可以
g(std::forward<decltype(vec)>(vec)); // 转发,保持值类别
 
for (auto&& x: f()) {
  // x 是转发引用;这是使用范围 for 循环最安全的方式
}
 
auto&& z = {1, 2, 3}; // *不是*转发引用(初始化器列表的特殊情形)

Type1&& var1=anotherType1Instance; // var1的类型是右值引用,但是作为左值
auto&& var2=var1;       //var2的类型是左值引用
std::vector<int> v;
auto&& val = v[0]; // std::vector::operator[]的返回值是元素左值,所以val的类型是左值引用

Widget makeWidget(); // 类工厂函数
Widget&& var1 = makeWidget() // var1的类型是右值引用,具有左值。
     
Widget var2 = static_cast<Widget&&>(var1); // var2在初始化时可以使用移动构造函数。

悬垂引用

尽管引用一旦初始化就始终指代一个有效的对象或函数,但有可能创建一个程序,其中被指代对象的生存期结束而引用仍保持可访问(悬垂(dangling))

std::string& f()
{
    std::string s = "Example";
    return s; // 退出 s 的作用域:调用其析构函数并解分配其存储
}
 
std::string& r = f(); // 悬垂引用
std::cout << r;       // 未定义行为:从悬垂引用读取
std::string s = f();  // 未定义行为:从悬垂引用复制初始化

移动语义

逐步理解

== 逐步理解该节阐述与【引子1】内容相似,但是是一种不同的角度看待问题。==
假设X是一个类,它持有指向某些资源的指针或句柄,例如m_pResource

X& X::operator=(X const & rhs)
{
  // [...]
  //对rhs.m_pResource所引用的内容做一个拷贝
  // 销毁m_pResource引用的资源。
  // 将克隆绑定到m_pResource
  // [...]
}
X foo();
X x;
x = foo();

x=foo()需要做的操作如下:

  1. 从foo返回的临时对象克隆资源,
  2. 销毁x持有的资源,并用克隆的对象替换它,
  3. 销毁临时对象,从而释放其资源。

很明显,在x和临时函数之间交换资源指针(句柄),然后让临时函数的析构函数析构x的原始资源,这样做是可以的,而且效率更高。换句话说,在这种特殊情况下右边的赋值是一个右值,我们希望赋值复制操作是这样的。

c++11可以通过重载赋值运算符实现:

X& X::operator=(<一个神秘的我了个去类型> rhs)
{
  // [...]
  // 将 this->m_pResource 和rhs.m_pResource交换
  // [...]  
}

由于定义了赋值操作符的重载,所以“一个神秘的我了个去类型”本质上必须是引用:我们当然希望右手边的值通过引用传递给我们。此外,我们预计“一个神秘的我了个去类型”会有以下行为:当需要在两个重载中进行选择,其中一个是普通引用,另一个是“一个神秘的我了个去类型”时,右值必须选择“一个神秘的我了个去类型”,而左值必须选择普通引用。
如下:

void foo(X& x); // 重载左值引用
void foo(X&& x); // 重载右值引用

X x;
X foobar();

foo(x); // 参数是左值: 调用 foo(X&)
foo(foobar()); // 参数是右值: 调用 foo(X&&)

以上便是我们写了这么多想要的移动语义的内容了。那么移动语义的定义具体是什么呢?

定义

查阅了很多资料,其实对于移动语义并未给出非常明确通用的定义。个人认为下面作者对移动语义理解挺好。所以直接截取了其中内容。
链接
在这里插入图片描述总结下来的定义应该为:将内存的所有权从一个对象转移到另外一个对象,高效的移动用来替换效率低下的复制。
至于很多人会用移动语义(std::move())这样的题目来说明移动语义,个人觉得有点误人子弟的意思。std::move()只能说是移动语义的一个很好的标准库例子。

移动构造函数和移动赋值运算符

为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。

移动构造函数和移动赋值运算符、拷贝构造函数和赋值运算符,这两个东西天生犯冲;如果自定义了前者后者默认是删除的,如果自定义了后者前者默认删除;所以如果自定义时最好都定义上。

移动构造函数

关键点
移动构造函数需要做的事情:
  1. 不分配任何新内存
  2. 接管给定的对象中的内存
  3. 接管对象后,将给定的对象的指针置为nullptr
  4. 完成了移动操作后,给定的对象将继续存在
  5. 我们可以对给定的对象进行销毁或者赋予新值,但是不能继续使用其值。
要素:
  1. 首个形参是 T&&、const T&&、volatile T&& 或 const volatile T&&
  2. 任何额外的形参的都必须有默认实参
  3. 确保移后源对象处于可被销毁状态,资源的所有权归属于新对象
  4. 为使强异常保证可行,用户定义的移动构造函数不应该抛出异常。例如,std::vector 在需要重新放置元素时,基于 std::move_if_noexcept 在移动和复制之间选择。

关于 第4条的说明如下

vector保证,如果我们调用push_back时发生异常,vector保证自身不变。
1] 对于push_back的调用可能会导致重新分配内存,如果在重新分配的过程使用了移动构造函数,且移动了部分而不是全部后抛出了异常,那么旧空间中的移动源已经改变,而新空间未构造的元素尚不存在,此种情况vector无法保证自身不变。
2] vector如果使用拷贝构造函数且发生异常,可以保证旧的vector不变。
3] 为了避免1]中情况出现,除非vector知道元素类型的移动构造函数不抛异常,否则在重新分配内存的过程中,他就必然使用拷贝构造函数而非移动构造函数。
4] 如果想在vector重新分配内存中使用移动而非拷贝,就需要使用noexcept来标记为不抛异常。

在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。

简单的示例

示例源自

#include <iostream>
using namespace std;
class demo{
public:
    demo():num(new int(0)){
        cout<<"construct!"<<endl;
    }
    demo(const demo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //添加移动构造函数
    demo(demo &&d)noexcept:num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
    ~demo(){
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};
demo get_demo(){
    return demo();
}
分类
合成的移动构造函数
  1. 若未对类类型提供自定义的移动构造函数,并且没有声明复制构造函数、赋值运算符、移动赋值运算符、析构函数,并且所有的非static数据成员都能移动构造或者移动赋值时,才合成默认的移动构造函数。
  2. 若有自定义的移动构造函数,仍然可以通过关键词 default 强制编译器生成隐式声明的移动构造函数。
  3. 合成的移动构造函数为类的非 explicit 的 inline public 成员,签名为 T::T(T&&)
//编译器会为X生成合成的移动构造函数
struct X{
	int i;//内置类型可以移动
	std::string s;//string有自己的移动操作
}
struct hasX{
	X mem;//X 有合成的移动操作
}

X x,x2 = std::move(x);// 使用合成的移动构造函数
hasX hx hx2 = std::move(hx);//使用合成的移动构造函数
自定义移动构造函数
#include <iostream>
#include <vector>
using namespace std;

class Move {
private:
	// 将指针声明为类的数据成员
	int* data;
public:
	// 构造函数
	Move(int d)
	{
		// 在堆中声明对象
		data = new int;
		*data = d;
		cout << "Constructor is called for "
			<< d << endl;
	};
	// 拷贝构造
	Move(const Move& source)
		: Move{ *source.data }
	{
		// 深拷贝复制数据
		cout << "Copy Constructor is called -"
			<< "Deep copy for "
			<< *source.data
			<< endl;
	}

	// 移动构造函数
	Move(Move&& source)
		: data{ source.data }
	{
		cout << "Move Constructor for "
			<< *source.data << endl;
		source.data = nullptr;
	}
	// 析构
	~Move()
	{
		if (data != nullptr)
			// 不为空
			cout << "Destructor is called for "
				<< *data << endl;
		else
			// data  为nullptr
			cout << "Destructor is called"
				<< " for nullptr "
				<< endl;
		// 释放分配给对象的data成员的内存
		delete data;
	}
};
int main()
{
	// Move 类Vector
	vector<Move> vec;
	// 插入数据
	vec.push_back(Move{ 10 });
	vec.push_back(Move{ 20 });
	return 0;
}

运行结果如下:

Constructor is called for 10
Move Constructor for 10
Destructor is called for nullptr 
Constructor is called for 20
Move Constructor for 20
Constructor is called for 10
Copy Constructor is called -Deep copy for 10
Destructor is called for 10
Destructor is called for nullptr 
Destructor is called for 10
Destructor is called for 20

移动赋值运算符

关键点
移动赋值运算符需要做的事情:

以下来源

  1. 定义一个空的赋值运算符,该运算符采用一个 对类类型的右值引用作为参数 并返回一个 对类类型的引用,如以下示例所示:
	MemoryBlock& operator=(MemoryBlock&& other)
	{
	}
  1. 在移动赋值运算符中,如果尝试将对象赋给自身,则添加不执行运算的条件语句。
    进行此检查的原因是此右值可能是move调用的返回结果。
	if (this != &other)
	{
	}
  1. 在条件语句中,从要将其赋值的对象中释放所有资源(如内存)。
    以下示例从要将其赋值的对象中释放 _data 成员:
	//释放现有资源
	delete[] _data;

执行第一个过程中的步骤 2 和步骤 3 以将数据成员从源对象转移到要构造的对象:

	// 从源对象复制数据指针及其长度。
	_data = other._data;
	_length = other._length;

	// 从源对象释放数据指针,这样析构函数就不会多次释放内存。
	other._data = nullptr;
	other._length = 0;
  1. 返回对当前对象的引用,如以下示例所示:
	return *this;
  1. 完整代码
	if (this != &other)
	{
		//释放当前对象的资源
		delete[] buf;
		size=0;
		// 占用other's 资源
		size=other.size;
		buf=other.buf;
		// 重置otther
		other.size=0;
		other.buf=nullptr;
	}
	return *this;
要素:
  1. 只接受一个类型为T&&、const T&&、volatile T&&或const volatile T&&的形参;
  2. 任何额外的形参的都必须有默认实参
  3. 确保移后源对象处于可被销毁状态,资源的所有权归属于新对象
  4. 如果没有用户声明的复制构造函数、移动构造函数、复制赋值运算符、析构函数 ,那么会生成合成的移动赋值运算符。如内联的public合成移动赋值运算符T& T::operator= (T&&)

继承体系中的移动语义

假设你写了一个类 Base, 你有 通过重载实现移动语义 Base的复制构造函数 和赋值运算符:

Base(Base const & rhs); // non-move semantics
Base(Base&& rhs); // move semantics

现在你写一个类 Derived源自 Base. 为了确保将移动语义应用于 Base部分 你的 Derived对象,你必须重载 Derived的副本 构造函数和赋值运算符也是如此。 让我们看看复制构造函数。 复制赋值运算符的处理方式类似。 左值的版本是 直截了当:

//左值版本
Derived(Derived const & rhs) 
  : Base(rhs)
{
  // Derived-specific stuff
}
//右值引用版本错误示范
Derived(Derived&& rhs) 
  : Base(rhs) // 错误: rhs是一个具名的右值引用,他是个左值
{
  // do somrthing
}
//右值引用版本正确释放
Derived(Derived&& rhs) 
  : Base(std::move(rhs)) // 正确, 调用Base(Base&& rhs)
{
   // do somrthing
}

移动语义与编译器优化

任何现代编译器都将对原始函数定义应用返回值优化。
假设X是一个重载赋值构造函数和拷贝构造函数以实现移动语义的类,也就是说具有自定义移动构造函数和移动赋值运算符的类。

X foo()
{
  X x;
  // 处理
  return x;
}

这里如果我们将return x;修改为return std::move(x);是否会更好呢?答案是否定的。编译器将直接在foo的返回值处构造X对象,而不是在本地构造一个X然后将其复制出来。很明显,这比move语义更好。

std::move()深入了解

我们不能将一个右值引用直接绑定到一个左值上,但我们可以通过std::move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。
例如int &&t1 = std::move(t0);//ok ``move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。但我们必须认识到,调用move就意味着承诺:除了对t0赋值和销毁外,我们不再使用它。

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

move的函数参数T&&是一个指向模板类型参数的右值引用。通过引用折叠,此参数与任何类型的实参匹配。特别是,我们即可以传递给move一个左值,也可以传递给它一个右值。
接下来从下面的例子让我们深入理解下std::move

string s1("hi"),s2;
s2 = std::move(string("bye!"));///> ok:从一个右值移动数据
s2 = std::move(s1);///>ok:但在赋值之后,s1的值是不确定的

传入右值

std::move(string("bye!"))推断如下:

  • 推断出T的类型为string
  • 因此,remove_referencestring 进行实例化
  • remove_reference<string>type成员是string
  • move的返回类型是string&&
  • move的函数参数t的类型为string&&

因此,这个调用的实例化move<string>,即函数 string && move(string &&tt),函数体返回static_cast<string &&>(t)t的类型已经是string &&,于是类型转化什么也不做。因此,此调用的结果就是它所接受的右值引用。

传入左值

std::move(s1)的推断如下:

  • 推断出的T的类型为string &(string的引用,而非普通的string)
  • 因此,remove_reference用string &进行实例化
  • remove_reference<string &>type成员是string
  • move的返回类型仍是string &&
  • move的函数参数t实例化为string &&,折叠为string &.
    因此,这个调用实例化move<string &>,即string && move(string &t)。这个实例的函数体返回static_cast<string &&>(t),在此情况下,t的类型为string &,static_cast将其转换为string &&

实质

std::move其实就是static_cast的显示转换。因为我们可以用static_cast显式地将一个左值转换为一个右值引用。

标准库容器的移动

以vector为例:
push_back从c++11开始支持移动版本

void push_back( const T& value );
void push_back( T&& value );

注:

若 T 的移动构造函数不是 noexcept 且 T 不可复制插入 (CopyInsertable) 到 *this ,则 vector 将使用会抛出的移动构造函数。若它抛出,则抛弃保证且效果未指定。

emplace_back函数定义如下:
template< class... Args > void emplace_back( Args&&... args );
注:

若抛出异常,则此函数无效果(强异常保证)。 若 T 的移动构造函数非 noexcept 且非可复制插入 (CopyInsertable) 到 *this ,则 vector 将使用抛出的移动构造函数。若它抛出,则保证被舍弃,且效果未指定。

#include <vector>
#include <iostream>
#include <iomanip>
 
int main()
{
    std::vector<std::string> letters;
    letters.push_back("abc");
    std::string s = "def";
    letters.push_back(std::move(s));///>移动作为参数
    std::cout << "vector holds: ";
    for (auto&& i : letters) std::cout << std::quoted(i) << ' ';
    std::cout << "\nMoved-from string holds " << std::quoted(s) << '\n';
}

运行结果如下:

vector holds: "abc" "def" 
Moved-from string holds ""

string的移动

代码来源

// move example
#include <utility>      // std::move
#include <iostream>     // std::std::cout
#include <vector>       // std::vector
#include <string>       // std::string
class String
{
	friend std::ostream& operator<<(std::ostream& os, const String& str);
public:
	String(void);
	String(const char* data, int length);
	String(const char* data);

	String(const String& from);//1 复制构造
	String& operator = (const String& from);//2 赋值操作符

	~String();//3 析构函数

	String(String&& from);//4 移动构造
	String& operator = (String&& from);//5 移动赋值操作符


protected:
	void clear(void);
	void copy(const char* data, size_t length);
private:
	char* m_data;
	size_t m_length;
	int m_id;
	static int s_i;
};
int String::s_i = 0;

std::ostream& operator<<(std::ostream& os, const String& str)
{
	for (size_t i = 0; i < str.m_length; ++i)
		os << str.m_data[i];
	return os;
}
String::String(void) :m_data(nullptr), m_length(0), m_id(++s_i)
{
	std::cout << "String(" << m_id << ")" << std::endl;
}
String::~String()
{
	std::cout << "~String(" << m_id << ")" << std::endl; clear();
}
String::String(const char* data, int _length) :m_data(nullptr), m_length(0), m_id(++s_i)
{
	std::cout << "String(const char*,int," << m_id << ")" << std::endl;
	copy(data, _length);
}
String::String(const char* data) : m_data(nullptr), m_length(0), m_id(++s_i)
{
	std::cout << "String(const char*," << m_id << ")" << std::endl;
	copy(data, strlen(data));
}
String::String(const String& from) : m_data(nullptr), m_length(0), m_id(++s_i)
{
	std::cout << "String(" << m_id << ", const String& " << from.m_id << " )" << std::endl;
	if (from.m_data != m_data)
	{
		copy(from.m_data, from.m_length);
	}
}
String& String::operator=(const String& from)
{
	std::cout << "String & String::operator=(const String &," << m_id << ")" << std::endl;
	if (&from != this)
	{
		copy(from.m_data, from.m_length);
	}
	return *this;
}
String::String(String&& from) :m_data(nullptr), m_length(0), m_id(++s_i)
{
	std::cout << "String(" << m_id << ", String&& " << from.m_id << " )" << std::endl;
	if (from.m_data != m_data)
	{
		std::swap(m_data, from.m_data);
		std::swap(m_length, from.m_length);
	}
}
String& String::operator=(String&& from)
{
	std::cout << "String & String::operator=(String &&," << m_id << ")" << std::endl;
	if (&from != this)
	{
		std::swap(this->m_data, from.m_data);//接管资源
		std::swap(this->m_length, from.m_length);
	}
	return *this;
}
void String::clear(void)
{
	if (nullptr != m_data)
	{
		delete[] m_data;
		m_data = nullptr;
		m_length = 0;
	}
}
void String::copy(const char* data, size_t length)
{
	std::cout << "clear new copy" << std::endl;
	clear();
	m_data = new char[length];
	for (size_t i = 0; i < length; ++i)
	{
		m_data[i] = data[i];
	}
	m_length = length;
}

String& GetLeftValue(void)
{
	static String s("Static String");
	return s;
}
String GetRightValue(void)
{
	String s("Static String");
	return s;
}
//左值常见的场景:p471
void TestLeftValue(void)
{
	auto& leftValue1 = GetLeftValue();//左值表达式:返回左值引用的函数是左值表达式
	String left2;
	left2 = (left2 = leftValue1);//左值表达式:赋值表达式返回左值
	std::vector<String> arrStr(2);
	left2 = (arrStr[0]);//左值表达式:下标返回左值
	String arr[2];
	left2 = (arr[0]);//左值表达式:下标返回左值
	auto p = arr;
	left2 = (*p);//左值表达式:接引用返回左值
	//++left2, --left2;//左值表达式:下标返回左值(如果用户重载该操作符的话)
	arrStr.push_back(left2);//左值表达式:左值变量未超出作用域之前变量名返回左值
}
//右值常见的场景:p471
void TestRightValue(void)
{
	String&& rightValue = GetRightValue();//右值表达式:返回非引用类型的函数是右值表达式(右值引用赋值给右值引用,延长右值的生命)
	const String& leftValue = GetRightValue();//右值表达式:返回非引用类型的函数是右值表达式,右值引用赋值给左值引用(移动构造)
	String leftValue1 = GetRightValue();//右值表达式:返回非引用类型的函数是右值表达式(移动构造)
	std::vector<String> arrStr;
	arrStr.emplace_back(GetRightValue());//右值表达式:返回非引用类型的函数是右值表达式,右值引用赋值给左值引用(移动构造)
	String leftValue2;
	leftValue2 = GetRightValue();//右值表达式:返回非引用类型的函数是右值表达式,右值赋给左值(移动赋值)
	String leftValue3(rightValue);//!!!!左值表达式:右值引用变量是变量表达式,变量都是左值。因为变量只有超出作用域才释放,是左值很合理(复制构造)!!!!!
	arrStr.push_back(rightValue);//!!!!左值表达式:右值引用变量是变量表达式,变量都是左值。因为变量只有超出作用域才释放,是左值很合理(复制构造)!!!!!
	{
		String leftValue3;
		String leftValue4("Hello World!");
		leftValue3 = std::move(leftValue4);//使用移动赋值
		std::cout << "test swap:" << std::endl;
		std::swap(leftValue3, leftValue4);//这里会资源交换,因为标准库std::swap优先尝试移动语义
	}
	//value1 + value2  //右值表达式:算术运算符返回右值(如果用户重载的话)
	//value1 - value2  //右值表达式:算术运算符返回右值(如果用户重载的话)
	//value1 * value2  //右值表达式:算术运算符返回右值(如果用户重载的话)
	//value1 / value2  //右值表达式:算术运算符返回右值(如果用户重载的话)

	//value1 < value2  //右值表达式:关系运算符返回右值(如果用户重载的话)
	//value1 > value2  //右值表达式:关系运算符返回右值(如果用户重载的话)
	//value1 <= value2  //右值表达式:关系运算符返回右值(如果用户重载的话)
	//value1 >= value2  //右值表达式:关系运算符返回右值(如果用户重载的话)
	//value1 == value2  //右值表达式:关系运算符返回右值(如果用户重载的话)
	//value1 != value2  //右值表达式:关系运算符返回右值(如果用户重载的话)

	//value1++ value1--  //右值表达式:后自增自减运算符返回右值(如果用户重载的话)
}
String GetValue(void)
{
	String a;
	String b;
	b = a;
	return a;//返回非引用类型的函数总是返回右值引用
}
String GetMoveValue(void)
{
	String a;
	String b;
	b = a;
	return std::move(a);//变量表达式总是一个左值p471, String&& move(String&)p608, move返回右值引用p609, 返回非引用类型的函数返回右值p471,右值引用的右值引用折叠为右值引用p609,所以函数返回右值引用
}
void TestMove(void)
{
	String value = GetValue();//移动构造:返回非引用类型的函数是右值表达式
	String value1 = GetMoveValue();//移动构造:返回非引用类型的函数是右值表达式
	String leftValue;
	auto value2 = std::move(leftValue);//String&& move(String&)
}
int main()
{
	TestLeftValue();
	TestRightValue();
	TestMove();

	std::cin.get();
	return 0;
}

shared_ptr移动

shared_ptr有如下几种初始化方式:

  • 裸指针直接初始化,但不能通过隐式转换来构造,因为shared_ptr构造函数被声明为explicit;
  • 允许移动构造,也允许拷贝构造;
  • 通过make_shared构造,在C++11版本中就已经支持了。
		std::shared_ptr<int> p3(new int(10));
    //调用拷贝构造函数
    std::shared_ptr<int> p4(p3);//或者 std::shared_ptr<int> p4 = p3;
    //调用移动构造函数
    std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4);
#include <iostream>
#include <memory>

class Frame {};

int main()
{
  std::shared_ptr<Frame> f1(std::move(new Frame()));        // 移动构造函数
  std::shared_ptr<Frame> f2 = std::move(new Frame());       // Error,explicit禁止隐式初始化
  std::shared_ptr<Frame> f3(std::move(f4));                 // 移动构造函数
  std::shared_ptr<Frame> f4 = std::move(f3);                // 移动构造函数
  return 0;
}

unique_ptr的移动

不能直接通过值给函数传递一个智能指针,因为通过值传递将导致复制真正的形参。如果要让函数通过值接收一个独占指针,则在调用函数时,必须对真正的形参使用 move() 函数:

//函数使用通过值传递的形参
void fun(unique_ptr<int> uptrParam)
{
    cout << *uptrParam << endl;
}
int main()
{
    unique_ptr<int> uptr(new int);
    *uptr = 10;
    fun (move (uptr)); // 在调用中使用 move
}

完美转发

完美转发为了解决什么?

看如下示例

示例1

#include <iostream>

template<typename T>
void print(T & t){
    std::cout << "Lvalue ref" << std::endl;
}

template<typename T>
void print(T && t){
    std::cout << "Rvalue ref" << std::endl;
}

template<typename T>
void testForward(T && v){ 
    print(v);//v此时已经是个左值了,永远调用左值版本的print
}

int main(int argc, char * argv[])
{
    int x = 1;
    testForward(x); //实参为左值
    testForward(std::move(x)); //实参为右值
}

运行如下:

Lvalue ref
Lvalue ref

查看文档之前的描述,testForward(T && v)这里v是具名的,因此是左值,因此不管右值还是左值永远永远是调用左值的版本print。

示例2

修改testForward(T && v)如下

template<typename T>
void testForward(T && v){ 
    print(std::move(v));
}

运行如下:

Rvalue ref
Rvalue ref

示例3

这就是为什么用std::forward的原由了

template<typename T>
void testForward(T && v){ 
    print(std::forward<T>(v));
}

运行结果如下

Lvalue ref
Rvalue ref

std::forward()深入了解

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

图片连接
在这里插入图片描述


注意事项

  • 应该将左值作为左值转发。
  • 应该将右值作为右值转发。
  • 不应 将 右值作为左值转发。
  • 应该将较少的 cv 限定表达式转发到更多 cv 限定的表达式。
#include <iostream>
#include <list>

template <class T>
struct C
{
    T t_;
    template <class U,
              class = typename std::enable_if
                    <
                        !std::is_lvalue_reference<U>::value
                    >::type>
        C(U&& u) : t_(std::forward<T>(std::move(u).get())) {}
};

class A
{
    int data_;
public:
    explicit A(int data = 1)
        : data_(data) {}
    ~A() {data_ = -1;}

    void test() const{
        if (data_ < 0) std::cout << "A is destructed\n";
        else std::cout << "A = " << data_ << '\n';
    }
};
class Awrap
{
    A& a_;
public:
    explicit Awrap(A& a) : a_(a) {}
    const A& get() const {return a_;}
          A& get()       {return a_;}
};

template <class C>
void test(C c)
{
    c.t_.test();
}

int main()
{
    std::list<C<const A&> > list;
    A a(3);
    C<const A&> c((Awrap(a)));
    list.push_back(c);
    test(c);
    test(list.front());
}
  • 应该将派生类型的表达式转发到可访问的、明确的基类型。
  • 不应 转发 任意类型转换。

参考文献

  1. http://thbecker.net/articles/rvalue_references/section_01.html
  2. https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
  3. https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Move_Constructor
  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-西门吹雪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值