[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源码 原理探究

C++标准库 STL -- 容器源码探索

C++的探索路20标准模板库STL之STL的基本概念与容器

C++的探索路20标准模板库STL之STL的基本概念与容器

C++ Primer 5th笔记(chap 16 模板和泛型编程)std::move

VC中function函数解析