【从零学C++11(中)】移动语义、右值引用、std::move()、完美转发等新特性


8. 默认函数控制

C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数、&和const&的重载、移动构造、移动拷贝构造等函数。

如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成

显式缺省函数

C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数

class A{
public:
	A(int a)
	 : _a(a)
	{}
	
	// 显式缺省构造函数,由编译器生成
	A() = default;
	
	// 可以选择在类中声明,在类外定义时让编译器生成默认赋值运算符重载
	A& operator=(const A& a);
private:
	int _a;
};

A& A::operator=(const A& a) = default;		//类外定义

int main(){
	A a1(10);
	A a2;
	a2 = a1;
	return 0;
}

删除默认函数

如果能想要限制某些默认函数的生成:

  • C++98中,是该函数设置成private,并且不完成实现,这样只要其他人想要调用就会报错。
  • C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
class A{
public:
	A(int a)
	 : _a(a)
	{}
	
	 // 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
	A(const A&) = delete;
	A& operator(const A&) = delete;
private:
	int _a;
};

int main(){
	A a1(10);
	A a2(a1);
	// 编译失败,因为该类没有拷贝构造函数
	
	A a3(10);	
	a3 = a2;	
	// 编译失败,因为该类没有赋值运算符重载
	
	return 0;
}

9. 右值引用【★】

移动语义

如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:

class String{
public:
	String(char* str = ""){
		if (nullptr == str)
			str = "";
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}

	String& operator=(const String& s){
		if (this != &s){
			char* pTemp = new char[strlen(s._str) + 1];
			strcpy(pTemp, s._str);
			delete[] _str;
			_str = pTemp;
		}
		return *this;
	}

	~String(){
		if (_str) delete[] _str;
	}
private:
	char* _str;
};

假设现在有一个函数,返回值为一个String类型的对象:

String GetString(char* pStr){
	String strTemp(pStr);
	return strTemp;		//此时不是返回栈上对象strTemp,而是拷贝构造一个临时对象返回
}
int main(){
	String s2(GetString("world"));	
	/* 用GetString返回的临时对象构造s2
		s2构造完成后临时对象将被销毁,因为临时对象临时对象不能直接返回
		因此编译器需要拷贝构造一份临时对象,然后将strTemp销毁
	*/
	return 0;
}

上述代码看起来没有什么问题,但是有一个不太尽人意的地方:GetString函数返回的临时对象,将s2拷贝构造成功之后,立马被销毁了(临时对象的空间被释放),再没有其他作用;
s2在拷贝构造时,又需要分配空间,一个刚释放一个又申请,有点多此一举。
那能否将GetString返回的临时对象的空间直接交给s2呢?这样s2也不需要重新开辟空间了,代码的效率会明显提高。

  • 将一个对象中资源移动到另一个对象中的方式,称之为移动语义
  • C++11中如果需要实现移动语义,必须使用右值引用
String(String&& s)		//两个 &
	: _str(s._str)
	{
		 s._str = nullptr; 
	} 

C++11中的右值

右值引用,顾名思义就是对右值的引用。C++11中,右值由两个概念组成:纯右值将亡值

  • 纯右值
    纯右值是C++98中右值的概念,用于识别临时变量一些不跟对象关联的值
    比如:常量一些运算表达式(1+3)等。
  • 将亡值
    声明周期将要结束的对象。比如:在值返回时的临时对象

右值引用

右值引用书写格式:

类型&& 引用变量名字 = 实体;

右值引用最长常见的一个使用地方就是:与移动语义结合,减少无必要资源的开辟来提高代码的运行效率

改造一下刚才的例子代码演示

String&& GetString(char* pStr){
	String strTemp(pStr);
	return strTemp;
}

int main(){
	String s1("hello");
	String s2(GetString("world"));
	return 0;
}

右值引用另一个比较常见的地方是:给一个匿名对象取别名,延长匿名对象的声明周期。

String GetString(char* pStr){
	return String(pStr);
}
int main(){
	String&& s = GetString("hello");
	return 0;
}

【注】:

  1. 与引用一样,右值引用在定义时必须初始化
  2. 通常情况下,右值引用不能引用左值
int main(){
	int a = 10;
	int&& ra; // 编译失败,没有进行初始化
	int&& ra = a; // 编译失败,a是一个左值
	
	const int&& ra = 10;	// ra是匿名常量10的别名
	return 0;
}

std::move()

C++11中,std::move()函数位于<utility> 头文件中,这个函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。

注意:被转化的左值,其生命周期并没有随着左右值的转化而改变,即std::move转化的左值变量left_value不会被销毁。

  • 下面举一个move()误用的例子:
// 移动构造函数
class String{
	String(String&& s) 
	 : _str(s._str){
		s._str = nullptr;
	}
};
int main(){
	String s1("hello world");
	String s2(move(s1));
	String s3(s2);
	return 0;
}

move()更多的是用在生命周期即将结束的对象上。

【注】:为了保证移动语义的传递,程序员在编写移动构造函数时,最好使std::move转移拥有资源的成员为右值。

注意点

  1. 如果将移动构造函数声明为常右值引用或者返回右值的函数声明为常量,都会导致移动语义无法实现。
String(const String&&);
const Person GetTempPerson();
  1. C++11中,无参构造函数 / 拷贝构造函数 / 移动构造函数实际上有3个版本:
Object();
Object(const T&);
Object(T &&);
  1. C++11中默认成员函数
    默认情况下,编译器会为程序员隐式生成一个(如果没有用到则不会生成)移动构造函数。如果程序员声明了自定义的构造函数、移动构造、拷贝构造函数、赋值运算符重载、移动赋值、析构函数,编译器都不会再为程序员生成默认版本。编译器生成的默认移动构造函数实际和默认的拷贝构造函数类似,都是按照位拷贝(即浅拷贝)来进行的。因此,在类中涉及到资源管理时,程序员最好自己定义移动构造函数。其他类有无移动构造都无关紧要。但在C++11中,拷贝构造/移动构造/赋值/移动赋值函数必须同时提供,或者同时不提供,程序才能保证类同时具有拷贝和移动语义。

完美转发

完美转发是指:在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

void Func(int x){
	// ......
}
template<typename T>
void PerfectForward(T t){
	Fun(t);
}

PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美:

  • 完美转发是:目标函数总希望将参数按照<传递给转发函数的实际类型>转给目标函数,而不产生额外的开销,就好像转发者不存在一样。

  • 所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。

C++11通过forward函数来实现完美转发, 比如:

void Fun(int &x) { cout << "lvalue ref" << endl; }
void Fun(int &&x) { cout << "rvalue ref" << endl; }
void Fun(const int &x) { cout << "const lvalue ref" << endl; }
void Fun(const int &&x) { cout << "const rvalue ref" << endl; }

template<typename T>
void PerfectForward(T &&t) { Fun(std::forward<T>(t)); }

int main(){
	PerfectForward(10); // rvalue ref
	
	int a;
	PerfectForward(a); // lvalue ref
	PerfectForward(std::move(a)); // rvalue ref
	const int b = 8;
	PerfectForward(b); // const lvalue ref
	PerfectForward(std::move(b)); // const rvalue ref
	return 0;
}

感谢您阅读至此,感兴趣的看官们可以移步上篇与下篇,继续了解C++11剩余新特性~

【从零学C++11(上)】列表初始化decltype关键字、委派构造等新特性
https://blog.csdn.net/qq_42351880/article/details/100140163

【从零学C++11(下)】lambda表达式、线程库原子操作库等新特性
https://blog.csdn.net/qq_42351880/article/details/100144882

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

giturtle

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

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

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

打赏作者

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

抵扣说明:

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

余额充值