设为首页 加入收藏

TOP

【重学C++】05 | 说透右值引用、移动语义、完美转发(下)(一)
2023-07-23 13:29:22 】 浏览:62
Tags:重学

文章首发

【重学C++】05 | 说透右值引用、移动语义、完美转发(下)

引言

大家好,我是只讲技术干货的会玩code,今天是【重学C++】的第五讲,在第四讲《【重学C++】04 | 说透右值引用、移动语义、完美转发(上)》中,我们解释了右值和右值引用的相关概念,并介绍了C++的移动语义以及如何通过右值引用实现移动语义。今天,我们聊聊右值引用的另一大作用 -- 完美转发

什么是完美转发

假设我们要写一个工厂函数,该工厂函数负责创建一个对象,并返回该对象的智能指针。

template<typename T, typename Arg> 
std::shared_ptr<T> factory_v1(Arg arg)
{
	return std::shared_ptr<T>(new T(arg));
}

class X1 {
public:
	int* i_p;
	X(int a) {
		i_p = new int(a);
	}
}

对于类X的调用方来说,auto x1_ptr = factory_v1<X1>(5); 应该与auto x1_ptr = std::shared_ptr<X>(new X1(5))是完全一样的。

也就是说,工厂函数factory_v1对调用者是透明的。要达到这个目的有两个前提:

  1. 传给factory_v1的入参arg能够完完整整(包括引用属性、const属性等)得传给T的构造函数。
  2. 工厂函数factory_v1没有额外的副作用。

这个就是C++的完美转发。

单看factory_v1应用到X1貌似很"完美",但既然是工厂函数,就不能只满足于一种类对象的应用。假设我们有类X2。定义如下

class X2 {
public:
	X2(){}
	X2(X2& rhs) {
		std::cout << "copy constructor call" << std::endl;
	}
}

现在大家再思考下面代码:

X2 x2 = X2();

auto x2_ptr1 = factory_v1<X2>(x2);
// output:
// copy constructor call
// copy constructor call

auto x2_ptr2 = std::shared_ptr<X2>(x2)
// output:
// copy constructor call

可以发现,auto x2_ptr1 = factory_v1<X2>(x2);auto x2_ptr2 = std::shared_ptr<X2>(x2)多了一次拷贝构造函数的调用。

为什么呢?很简单,因为factory_v1的入参是值传递,所以x2在传入factory_v1时,会调用一次拷贝构造函数,创建arg。很直接的办法,把factory_v1的入参改成引用传递就好了,得到factory_v2

template<typename T, typename Arg> 
std::shared_ptr<T> factory_v2(Arg& arg)
{
	return std::shared_ptr<T>(new T(arg));
}

改成引用传递后,auto x1_ptr = factory_v2<X1>(5);又会报错了。因为factory_v2需要传入一个左值,但字面量5是一个右值。

方法总比困难多,我们知道,C++的const X& 类型参数,既能接收左值,又能接收右值,所以,稍加改造,得到factory_v3

template<typename T, typename Arg> 
std::shared_ptr<T> factory_v3(const Arg& arg)
{
	return std::shared_ptr<T>(new T(arg));
}

factory_v3还是不够"完美", 再看看另外一个类X3

class X3 {
public:
	X3(){}
	X3(X3& rhs) {
		std::cout << "copy constructor call" << std::endl;
	}

	X3(X3&& rhs) {
		std::cout << "move constructor call" << std::endl;
	}
}

再看看以下使用例子

auto x3_ptr1 = factory_v3<X3>(X3());
// output
// copy constructor call

auto x3_ptr2 = std::shared_ptr<X3>(new X3(X3()));
// output
// move constructor call

通过上一节我们知道,有名字的都是左值,所以factory_v3永远无法调用到T的移动构造函数。所以,factory_v3还是不满足完美转发。

特殊的类型推导 - 万能引用

给出完美转发的解决方案前,我们先来了解下C++中一种比较特殊的模版类型推导规则 - 万能引用。

// 模版函数签名
template <typename T>
void foo(ParamType param);

// 应用
foo(expr);

模版类型推导是指根据调用时传入的expr,推导出模版函数fooParamTypeparam的类型。

类型推导的规则有很多,大家感兴趣可以去看看《Effective C++》[1],这里,我们只介绍一种比较特殊的万能引用。 万能引用的模版函数格式如下:

template<typename T>
void foo(T&& param);

万能引用的ParamTypeT&&,既不能是const T&&,也不能是std::vector<T>&&

万能引用的规则有三条:

  1. 如果expr是左值,Tparam都会被推导成左值引用。
  2. 如果expr是右值,T会被推导成对应的原始类型,param会被推导成右值引用(注意,虽然被推导成右值引用,但由于param有名字,所以本身还是个左值)。
  3. 在推导过程中,expr的const属性会被保留下来。

看下面示例

template<typename T>
void foo(T&& param);
// x是一个左值
int x=27;
// cx是带有const的左值
const int cx = x;
// rx是一个左值引用
const int& rx = cx;

// x是左值,所以T是int&,param类型也是int&
foo(x);

// cx是左值,所以T是const int&,param类型也是const int&
foo(cx);

// rx是左值,所以T是const int&,param类型也是con
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇【QCustomPlot】性能提升之修改源.. 下一篇驱动开发:内核解析PE结构导出表

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目