【重学C++】05 | 说透右值引用、移动语义、完美转发(下)
文章首发
【重学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对调用者是透明的。要达到这个目的有两个前提:
- 传给
factory_v1的入参arg能够完完整整(包括引用属性、const属性等)得传给T的构造函数。 - 工厂函数
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,推导出模版函数foo中ParamType和param的类型。
类型推导的规则有很多,大家感兴趣可以去看看《Effective C++》[1],这里,我们只介绍一种比较特殊的万能引用。 万能引用的模版函数格式如下:
template<typename T>
void foo(T&& param);
万能引用的
ParamType是T&&,既不能是const T&&,也不能是std::vector<T>&&
万能引用的规则有三条:
- 如果
expr是左值,T和param都会被推导成左值引用。 - 如果
expr是右值,T会被推导成对应的原始类型,param会被推导成右值引用(注意,虽然被推导成右值引用,但由于param有名字,所以本身还是个左值)。 - 在推导过程中,
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类型也是const int&
foo(rx);
// 27是右值,所以T是int,param类型就是int&&
foo(27);
std::forward实现完美转发
到此,完美转发的前置知识就已经讲完了,我们看看C++是如何利用std::forward实现完美转发的。
template<typename T, typename Arg>
std::shared_ptr<T> factory_v4(Arg&& arg)
{
return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
std::forward的定义如下
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
return static_cast<S&&>(a);
}
传入左值
X x;
auto a = factory_v4<A>(x);
根据万能引用的推导规则,factory_v4中的Arg会被推导成X&。这个时候factory_v4和std::forwrd等价于:
shared_ptr<A> factory_v4(X& arg)
{
return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& std::forward(X& a)
{
return static_cast<X&>(a);
}
这个时候传给A的参数类型是X&,即调用的是拷贝构造函数A(X&)。符合预期。
传入右值
X createX();
auto a = factory_v4<A>(createX());
根据万能引用推导规则,factory_v4中的Arg会被推导成X。这个时候factory_v4和std::forwrd等价于:
shared_ptr<A> factory_v4(X&& arg)
{
return shared_ptr<A>(new A(std::forward<X>(arg)));
}
X&& forward(X& a) noexcept
{
return static_cast<X&&>(a);
}
此时,std::forward作用与std::move一样,隐藏掉了arg的名字,返回对应的右值引用。这个时候传给A的参数类型是X&&,即调用的是移动构造函数A(X&&),符合预期。
总结
这篇文章,我们主要是继续第四讲的内容,一步步学习了完美转发的概念以及如何使用右值解决参数透传的问题,实现完美转发。
[1] https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/1.DeducingTypes/item1.md
END
【往期推荐】
【重学C++】02 | 脱离指针陷阱:深入浅出 C++ 智能指针
【重学C++】04 | 说透C++右值引用、移动语义、完美转发(上)
【重学C++】05 | 说透右值引用、移动语义、完美转发(下)的更多相关文章
- 对C++11中的`移动语义`与`右值引用`的介绍与讨论
本文主要介绍了C++11中的移动语义与右值引用, 并且对其中的一些坑做了深入的讨论. 在正式介绍这部分内容之前, 我们先介绍一下rule of three/five原则, 与copy-and-swap ...
- 【C/C++开发】C++11:右值引用和转发型引用
右值引用 为了解决移动语义及完美转发问题,C++11标准引入了右值引用(rvalue reference)这一重要的新概念.右值引用采用T&&这一语法形式,比传统的引用T&(如 ...
- 详解 C++ 左值、右值、左值引用以及右值引用
一.左值和右值 1.左值 [可以取地址的对象就是左值] 左值是一个表示数据的表达式,比如:变量名.解引用的指针变量.一般地,我们可以获取它的地址和对它赋值,但被 const 修饰后的左值,不能给它赋值 ...
- Effective Modern C++:05右值引用、移动语义和完美转发
移动语义使得编译器得以使用成本较低的移动操作,来代替成本较高的复制操作:完美转发使得人们可以撰写接收任意实参的函数模板,并将其转发到目标函数,目标函数会接收到与转发函数所接收到的完全相同的实参.右值引 ...
- item 24: 区分右值引用和universal引用
本文翻译自<effective modern C++>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 古人曾说事情的真相会让你觉得很自在,但是在适当的情 ...
- C++11中的右值引用及move语义编程
C++0x中加入了右值引用,和move函数.右值引用出现之前我们只能用const引用来关联临时对象(右值)(造孽的VS可以用非const引用关联临时对象,请忽略VS),所以我们不能修临时对象的内容,右 ...
- [转载]如何在C++03中模拟C++11的右值引用std::move特性
本文摘自: http://adamcavendish.is-programmer.com/posts/38190.htm 引言 众所周知,C++11 的新特性中有一个非常重要的特性,那就是 rvalu ...
- C++11之右值引用(二):右值引用与移动语义
上节我们提出了右值引用,可以用来区分右值,那么这有什么用处? 问题来源 我们先看一个C++中被人诟病已久的问题: 我把某文件的内容读取到vector中,用函数如何封装? 大部分人的做法是: v ...
- C++11常用特性介绍——左值引用、右值引用
一.左值.右值 1)左值:可以放在赋值号左侧.可以被赋值的值:左值必须要在内存中有实体. 2)右值:必须放在赋值号右侧.取出值赋值给其它变量:右值可以在内存中也可以在CPU寄存器中. 二.引用 引用是 ...
- C++左值引用与右值引用
本文翻译自:https://docs.microsoft.com/en-us/cpp/cpp/references-cpp?view=vs-2019 引用,类似于指针,用于存储一个位于内存某处的对象的 ...
随机推荐
- 快速带你复习html(超详细)
此内容包含: html基础 列表.表格 媒体元素 表单(重点) 1.HTML 基础 目标: 会使用HTML5的基本结构创建网页 会使用文本相关标签排版文本信息 会使用图像相关标签实现图文并茂的页面 会 ...
- Go语言:利用 TDD 逐步为一个字典应用创建完整的 CRUD API
前言 在数组这一章节中,我们学会了如何按顺序存储值.现在,我们再来看看如何通过键存储值,并快速查找它们. Maps 允许你以类似于字典的方式存储值.你可以将键视为单词,将值视为定义. 所以,难道还有比 ...
- CSAPP-Attack Lab
目录 Code Injection Attacks Level 1 Level 2 Level_3 Return-Oriented Programming Level_4 Level_5 获取栈顶地址 ...
- windows下使用pytorch进行单机多卡分布式训练
现在有四张卡,但是部署在windows10系统上,想尝试下在windows上使用单机多卡进行分布式训练,网上找了一圈硬是没找到相关的文章.以下是踩坑过程. 首先,pytorch的版本必须是大于1.7, ...
- Mybatis 获取自增主键 useGeneratedKeys与keyProperty 解答
Mybatis 获取自增主键 今天开发的时候遇到一个疑惑,业务场景是这样的, 但是百度好久没有找到合适的解答,于是自己向同事了解,感觉还不错,因此写上了这个文章 有一个表A和一个表B A就是一个主表, ...
- 一步步制作下棋机器人之 coppeliasim进行Scara机械臂仿真与python控制
稚晖君又发布了新的机器人,很是强大. 在编写时看到了稚晖君的招聘信息,好想去试试啊! 小时候都有一个科幻梦,如今的职业也算与梦想有些沾边了.但看到稚晖君这种闪着光芒的作品,还是很是羡慕. 以前就想做一 ...
- 二进制安装Kubernetes(k8s) v1.24.3 IPv4/IPv6双栈
二进制安装Kubernetes(k8s) v1.24.3 IPv4/IPv6双栈 Kubernetes 开源不易,帮忙点个star,谢谢了 介绍 kubernetes(k8s)二进制高可用安装部署,支 ...
- Java GenericObjectPool 对象池化技术--SpringBoot sftp 连接池工具类
Java BasePooledObjectFactory 对象池化技术 通常一个对象创建.销毁非常耗时的时候,我们不会频繁的创建和销毁它,而是考虑复用.复用对象的一种做法就是对象池,将创建好的对象放入 ...
- flask-login使用方法
烧瓶登录 Flask-Login 为 Flask 提供用户会话管理.它处理登录.注销和长时间记住用户会话的常见任务. 它会: 将活动用户的 ID 存储在Flask Session中,让您轻松登录和注销 ...
- pandas之窗口函数
为了能更好地处理数值型数据,Pandas 提供了几种窗口函数,比如移动函数(rolling).扩展函数(expanding)和指数加权函数(ewm).窗口函数应用场景非常多.举一个简单的例子:现在有 ...