[C++标准库探索] 解析C++ move forward swap源码 原理探究
Posted Test_233
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C++标准库探索] 解析C++ move forward swap源码 原理探究相关的知识,希望对你有一定的参考价值。
c++标准库选用gcc 4.8 以下标准库源码来源于bits/move.h
1.std::move
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{
return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}
move是一个函数模板 模板参数为_Tp 功能是无条件获得指定对象的右值引用
由move的具体实现可以看出 move本质是一个强制类型转换 以static_cast完成
看到move的参数列表 该函数仅接收一个参数 注意到该参数类型是_Tp&& 这是一个万能引用
万能引用是一种既可以绑定到左值,又可以绑定到右值的类型,它几乎可以绑定到万事万物
那么move即意为无论接受的对象类型是什么样的 都会返回这个对象的右值引用 也就是"无条件"的体现
move是一个声明了noexcept的函数 这指明该函数不会抛出异常
看到一个重要的部分 返回值部分
返回值应用了std::remove_reference<_Tp>::type 这是标准库中提供的称为type_traits的工具
type_traits也可以译作类型萃取 用于在编译期进行某些类型转换 实现的核心技术是借助类模板和模板特化
std::remove_reference<_Tp>用于移除_Tp所带的引用类型 无论是左值引用还是右值引用
移除引用后 无论_Tp原来是什么 也许是单一的类型(如:int) 也许是带引用的类型(如:int& int&&) 现在都将是单一的类型(如int) 那么在这个单一的类型后跟上&& 那就表示了这个类型的右值引用(如:int&&) 这样是我们想得到的返回值类型 所以在此应用了引用移除技术 即std::remove_reference<_Tp>
std::remove_reference<_Tp>移除后的结果是通过type进行访问 type本质是一个typedef类型别名
下面给出标准库中实现std::remove_reference<_Tp>的源码 可以看出有两个针对左右值引用的特化版本
/// 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; };
最后return时通过static_cast将指定对象强制转换成其右值引用类型
2.std::swap
#if __cplusplus >= 201103L
#define _GLIBCXX_MOVE(__val) std::move(__val)
#else
#define _GLIBCXX_MOVE(__val) (__val)
#endif
template<typename _Tp>
inline void
swap(_Tp& __a, _Tp& __b)
{
_Tp __tmp = _GLIBCXX_MOVE(__a);
__a = _GLIBCXX_MOVE(__b);
__b = _GLIBCXX_MOVE(__tmp);
}
首先可以看出 标准库对于不同的C++版本有不同的实现 体现在#define _GLIBCXX_MOVE(__val)
无论如何 swap都接受两个参数 即需要相互交换的两个对象的引用
swap之前声明了inline 所以有可能的话编译器会解析为内联函数 从而优化掉函数调用的开销
对于C++11或更高版本 由于引入了移动的概念 那么swap便可以基于此进行优化处理
优化是建立在std::move的基础上的 我们知道move可以得到对象的右值引用
借用右值引用 我们就可以将对象资源"移"过去 而省去了拷贝所引发的不必要的资源浪费
打个比方 小红和小明两人都有一本课堂笔记 现在两人想交换笔记 如何实现呢?
在没有"移"的概念之前 应该是这样的 小红先将自己的笔记抄一本新的出来 再将小明笔记上的内容抄到旧的那本笔记中 最后让小明把小红新的笔记上的内容抄到自己的笔记本上 这么一看 是不是多了很多没有必要的抄写功夫
那么现在应用了移动 那么很直接的 小明把笔记本交给小红 小红把笔记本交给小明 就完成工作了
回到std::swap 如果交换的是用户自己定义的class 那么当用户实现了移动赋值运算符 那么swap本质用的是移动赋值运算符 如果没有实现移动赋值运算符 那么swap使用的就是拷贝赋值运算符 而拷贝赋值运算符意味着又有构造也有析构 这就是资源浪费的根源 因为是有机会避免进行这些构造析构的
预处理器会根据当前C++版本选择 _GLIBCXX_MOVE(val) 的解析值 若是支持移动语义的 即C++11或更高版本 那么_GLIBCXX_MOVE(val)会解析到std::move(val) 反之解析到val 不同就在于根据版本选用swap的底层操作是移动还是拷贝
swap的具体实现过程如下:
首先swap会将a中的数据移到临时对象temp中,然后再将b中的数据移到a中,最后再将temp中的数据,也就是原先的a的数据,移到b中,此时就实现了a和b交换数据
3.std::forward
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{
return static_cast<_Tp&&>(__t);
}
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);
}
std::forward有两个实现版本 一个是针对左值的 一个是针对右值的 当然我们常用的是第一个版本 即针对左值的版本 因为std::forward主要用于配合万能引用使用 我们称作完美转发(perfect forwarding)
std::forward的实现基于一个原则 即引用折叠原则
int x = 10;
auto& &rx = x;//错误 不能声明引用的引用
我们不能自己使用引用的引用 但是编译器可以 在特殊的语境下(如:模板实例化) 编译器会产生引用的引用 那怎么处理这样的现象呢?答案是引用折叠原则
引用折叠规则:如果任一引用是左值引用 则结果为左值引用 否则结果为右值引用(即两个引用都是右值引用结果为右值引用)
T& && i;//左值引用+右值引用 结果为左值引用
T& &j;//左值引用+左值引用 结果为左值引用
T&& &g;//右值引用+左值引用 结果为左值引用
T&& &&k;//右值引用+右值引用 结果为右值引用
注意到上述结果 得到右值引用仅有一种可能 即右值引用+右值引用(4个&)
为了方便测试 将std::forward改写成forward_s
注意forward_s的实现原理与std::forward无差 在不至歧义的情况下 forward_s等同于std::forward
我们针对不同情况来调用forward_s 实现如下:
#include <iostream>
#include <type_traits>
using namespace std;
template<typename _Tp>
const _Tp&&
forward_s(typename std::remove_reference<_Tp>::type& __t) noexcept
{
std::cout << "version:1" << std::endl;
return static_cast<_Tp&&>(__t);
}
template<typename _Tp>
const _Tp&&
forward_s(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
std::cout << "version:2" << std::endl;
if(!std::is_lvalue_reference<_Tp>::value)
return static_cast<_Tp&&>(__t);
}
template<class T>
void f(T&& t){
forward_s<T>(t);
//无论传入f的是左值还是右值 只要在f中使用 由于具名t 那么就是个左值
//则调用version1 即左值引用的版本
}
int main(){
int i = 10;
f(i);//给f传入左值
f(10);//给f传入右值
f(std::move(i));//给f传入右值
//传入左值 调用version1
forward_s<int&>(i);
//传入右值 调用version2 即右值引用的版本
forward_s<int>(std::move(i));
forward_s<int>(10);
return 0;
}
测试结果:
version:1
version:1
version:1
version:1
version:2
version:2
结果分析:
1.f(i)将i传入f i的类型是int且是个左值 由于f是个函数模板 此时将涉及到模板类型推导 即编译器将得出T到底是个什么类型
首先会出现f(int& && t) 因为对于万能引用形参 初始化对象是个左值时 形参类型推导为左值引用 由于引用折叠原则 实际上会是f(int& t) 由此决定模板参数推导:T = int&
继续下去则会如此调用forward<int&>(t) 也就是forward的模板参数_Tp被指定为int&
而forward的返回值类型是_Tp&& 此时具体为int& && 再次使用引用折叠原则 得到int& 也就是最终的返回类型会是int& 一个左值引用 最终forward的推导结果如下(注意type_traits的引用移除):
const int& &&
forward_s(typename std::remove_reference<int&>::type& __t) noexcept
{
std::cout << "version:1" << std::endl;
return static_cast<int& &&>(__t);
}
const int&
forward_s(int& __t) noexcept
{
std::cout << "version:1" << std::endl;
return static_cast<int&>(__t);
}
对于传入是右值时的情况 推导规则是类似的:
首先会出现f(int&& && t) 这里是形参的类型推导 根据引用折叠原则 实际是f(int&& t) 此时再进行模板参数的推导 因为原始签名为f(T&& t) 对比此时f(int&& t) 将有T = int 然后将调用forward<int>(t) 进而返回int&& 一个右值引用
显然 forward是根据传入的模板参数来决定返回左值引用还是右值引用的 对比起std::move std::forward是一个"有条件的"强制类型转换
以上是关于[C++标准库探索] 解析C++ move forward swap源码 原理探究的主要内容,如果未能解决你的问题,请参考以下文章
[C++标准库探索] 解析C++ move forward swap源码 原理探究