目录
(3)定义自己的hashfunction------------一个万用的hashfunction(本部分内容缺失,有空对着PPT学一下)
一、C++关键字
二、标准库源代码分布
1 VC的编译器源码目录
C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include
2 GNU C++的编译器源码目录
三、Rvalue references(右值引用)
Rvalue references are a new reference type introduced in C++ that help solve the problem of unnecessary copying and enable perfect forwarding. When the right-hand side of an assignment is an rvalue, then the left-hand side object can steal resources from the right-hand side object rather than performing a seperate allocation, thus enabling move semantics.
(注:perfect forwarding被翻译为精确传递,精确转发等,其含义为将一组参数原封不动地传递给另一个函数。在这里“原封不动”不仅仅是参数的值不变,除了参数值之外,还有两组属性:左值 / 右值和 const / non-const, 精确传递就是在参数传递过程中,所有这些属性和参数值都不能改变。第二部分也会具体解释。
另参见C++11之右值引用、完美转换(Perfect Forwarding)、std::move_云飞扬_Dylan的博客-CSDN博客)
1. 左值和右值的定义
-
左值lvalue:可以出现在operator=左边的值;(表达式结束后依然存在的对象,我们也叫做变量;可以出现在=左边也可以在右边。)
-
右值rvalue:只能出现在operator=右边的值。(表达式结束后就不存在的临时对象;只能出现在=右边;例如临时对象。)
比如说临时对象就是一种右值,形似:
int a = 0, b = 0;
a + b = 4;
不能通过编译,因为a+b是右值。
但例外的是,很多class的临时对象竟然可以被赋值,以string为例:
string s1("Hello ");
string s2("World");
s1 + s2 = s2; //竟然可以通过编译
//赋值之后s1, s2的值不变
string() = "World"; //竟然可以对temp obj(临时对象)赋值
但是这只是一种例外情况,可以理解为语言本身的“bug”,临时对象是永远被当做右值的,即使有些class的临时对象可以放在等号左边(“可以”指可以通过编译,给临时对象赋值无意义)。
【注意】:
虽然string和复数的举例推翻了左右值书写的准则,但是这是由于一些C++定义类型导致的,我们不要去管他;
我们只要记住两点:
-
1、临时对象就是一个右值;
-
2、右值不要出现在等号左边。
2. 为什么要引入一个新的"右值引用"的概念
【左值引用】:
对左值的引用即为左值引用,对右值的引用即为右值引用。需要注意得是引用是对值得别名,所以不会产生副本,同时引用本身也是变量,所以也是左值。如下:
int &t = a; // a为左值,所以可以赋给左值引用
// int &t1 = 3; // 错误 3为一个临时值,为右值,不能赋给左值引用
// int &&t = a; // 错误 a为左值,不能赋给右值引用
int &&t = 3; // 可以
int &&t = -a; // 可以
// int &t = -a; // 不可以
// int &&t1 = t; // 不可以,t本身是左值
【右值引用】:
- 一种新的引用类型,可以减少不必要的拷贝。
- 当右手边是一个右值时,左手边可以偷右边的资源,而不需要copy。
- 当rvalue出现于operator=(copy assignment)的右侧,我们认为对其资源进行偷取 / 搬移(move)而非拷贝(copy)是可以的,是合理的。
那么:
(1)必须有语法让我们在调用端告诉编译器:这是个rvalue
(2)必须有语法让我们在被调用端写出一个专门处理rvalue的所谓move assignment函数
3. 一个例子
(1)insert()往容器中插入元素,涉及到元素的移动,所以为了效率insert()有右值引用的版本,搬动元素的时候要调用元素的ctor,调用的ctor就是元素的move ctor;
(2)一个对象的move ctor的逻辑:简单地copy 指针的值,所以原来的对象对资源的引用要销毁,要保证原来的对象不再使用,这样才安全;
(3)使用std::move()可以得到一个左值的右值引用;
在C++11之前我们对一个函数返回值取地址是错误的;但是在C++11的新语法中,我们可以使用&&符号表示对右值取引用或者使用move函数将一个左值变为右值;相应的,我们也要为对应的元素对象实现一个move构造函数或者move赋值函数的重载版本(适用容器中操作元素时)。比如在做容器的在C++ 2.0之后,容器的插入动作都提供了一个insert的重载版本,专门适用这种新语法,如下所示:
当编译器检测到我们insert的值是一个右值(move函数返回一个右值)或者右值引用(&&,临时对象都会被当成右值引用)时,会调用下面新增的这个重载函数,让它偷取这个右值的东西免去自身取构造内存的动作,因为插入动作会调用拷贝构造函数,如果插入的元素是一个基本类型而不需要额外提供什么,但是如果插入的是一个复杂类型,原本我们需要写一个拷贝构造函数,开辟一块内存一个个的赋值过去,但现在我们要提供一个move搬移构造就行了(比如像string(编译器已经实现)类,move构造函数只是将既有的内部字符数组赋予新对象就行了,此时相当于新对象指针和原对象指针指向同一个地方,要注意执行move后原对象的指针是个不确定状态,不能使用),所以任何非平凡的类(除了基本类型),都应该提供move构造和move assignment(赋值)函数:
-
这里说一下move中的偷的概念,所谓偷就是借用之前的值,对于指针来说就是两个指针指向同一个地方,也就是说move语义就是指针的浅拷贝,为了指针的安全我们还要在偷完之后将原来的指针打断以禁止后续再使用这个值
-
右值经函数转交到下一个函数时会变成一个左值
四、Perfect forwarding(完美转发)
这里只说了怎么写可以实现完美转发,但是没有解释为什么需要这么写?以及各种不同类型(左值还是右值,const还是non-const)的情况是怎样得到正确处理的?得去别的地方找一下。
1、标准库中forward()和move()的定义
含义:
move和forward都是C++11中引入的,它们是移动语义和完美转发实现的基石。
-
move:不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义
- 从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue)
-
forward: 不转发任何东西,也是执行左值到右值的强制类型转换,只是仅在特定条件满足时才执行该转换
- 典型使用场景:某个函数模板取用了万能引用类型为形参,随后把它传递给了另一个函数
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); }
forward():
//Forward on lvalue
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
//Forward on rvalue
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);
}
2、不完美转发
3、完美转发
4、引用折叠规则
引用折叠规则用于类型推导。
引用折叠规则:
X& & <=> X&, 左值引用的左值引用等价于左值引用
X& && <=> X&, 左值引用的右值引用等价于左值引用
X&& & <=> X&, 右值引用的左值引用等价于左值引用
X&& && <=> X&&,右值引用的右值引用等价于右值引用
在C++中,直接声明引用的引用是不合法的,但允许 “间接” 声明引用的引用却是被允许的。尽管如此,“间接” 声明的引用的引用还是要被转换成单一引用。转换的规则是引用折叠规则。引用折叠规则会在以下四种上下文中用到:
- 模板参数的实例化,这种情况下引用折叠规则被用来推断出一个合法的类型参数。请看代码:
template <typename T> void test(T &&val) {} int i = 0; const int ci = i; test(i); // T被推断为 int&,val的类型是 int& &&,折叠为 int& test(ci); // T被推断为 const int&,val的类型是 const int& &&,折叠为 const int& test(i * ci); // T被推断为 int,val的类型是 int&&
- typedef类型别名,这种情况下引用折叠规则被用来处理 “间接” 声明的引用的的引用。请看代码:
typedef int &&INTR; // int类型的右值引用类型 INTR &&intrr = 5; // 右值引用的右值引用,折叠为右值引用
- auto类型说明符,这种情况下与第一种情况类似,引用折叠规则被用来推断出一个合法的类型。请看代码:
int a = 10; auto &&b = a; // auto类型被推断为 int&,b的类型为 int& &&,折叠为 int& auto &c = 8; // 错误,即使利用引用折叠规则也无法推断出合法的类型
- decltype类型指示符,这种情况下与第二种情况类似,引用折叠规则被用来处理 “间接” 声明的引用的引用。请看代码:
int a = 5; decltype((a)) &&b = a; // decltype推断出的类型是 int&,b的类型是 int& &&,折叠为 int&
五、写一个move-aware class
(1)定义了Big5和hashfunction,拷贝构造和拷贝赋值的逻辑很重要,具体见代码;
(2)move constructor和move assignment operator本质上都是浅拷贝,但在浅拷贝完成以后要把原先对象与资源的联系切断,在本例中表现为将指针置为NULL。所以在析构函数中,释放资源之前要先判断指针是否为NULL;
class MyString{
public:
static size_t DCtor; //累计default-ctor呼叫次数
static size_t Ctor; //累计ctor呼叫次数
static size_t CCtor; //累计copy-ctor呼叫次数
static size_t CAsgn; //累计copy-asgn呼叫次数
static size_t MCtor; //累计move-ctor呼叫次数
static size_t MAsgn; //累计move-asgn呼叫次数
static size_t Dtor; //累计dtor呼叫次数
private:
char* _data;
size_t _len;
void init_data(const char *s){
_data = new char[_len + 1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
//default constructor
MyString() : _data(NULL), _len(0) { ++DCtor; }
//constructor
MyString(const char* p) : _len(strlen(p)){
++Ctor;
_init_data(p);
}
//copy constructor
MyString(const MyString& str) : _len(str.len){
++CCtor;
_init_data(str._data);
}
//move constructor, with "noexcept"
MyString(MyString&& str) noexcept
: _data(str._data), _len(str._len){
++MCtor;
str._len = 0;
str.data = NULL; //重要
}
//copy assignment
MyString& operator=(const MyString& str){
++CAsgn;
if(this != &str){
if(_data) delete _data;
_len = str._len;
_init_data(str._data);
}
else{
}
return *this;
}
//move assignment
MyString& operator=(MyString&& str) noexcept{
++MAsgn;
if(this != &str){
if(_data) delete _data;
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
return *this;
}
//dtor
virtual ~MyString(){
++Dtor;
if(_data){
delete _data;
}
}
bool operator<(const MyString& rhs) const
{
return string(this->_data) < string(rhs._data);
}
bool operator==(const MyString& rhs) const
{
return string(this->_data) == string(rhs._data);
}
char* get() const { return _data; }
};
size_t MyString::DCtor = 0;
size_t MyString::Ctor = 0;
size_t MyString::CCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MCtor = 0;
size_t MyString::MAsgn = 0;
size_t MyString::Dtor = 0;
namespace std //必须放在std内
{
template<>
struct hash<MyString>{ //for unordered containers
size_t operator()(const MyString& s) const noexcept
{ return hash<string>() (string(s.get())); }
};
六、使用三中定义的MyString测试容器的效能
测试内容:
(1)分别使用定义了move语义的和没有定义move语义的class,对容器进行多次插入操作(都只在尾部插入)
(2)测试拷贝容器,move容器,swap两个容器的效率
测试结果:
(1)vector的两种时间差别巨大,因为vector要扩容,所以涉及很多拷贝操作,使用move会大大节省时间,而对于list,deque,multiset和unordered_multiset两种时间差别不大。但其实按理说deque也可能要搬动元素,但本例中插入的元素都在尾部,所以不涉及搬动操作。
(2)所有容器都体现出:copy容器耗时很多,move和swap容器几乎不耗时。这是因为copy容器需要分配空间并依次拷贝元素,但是move容器仅仅是交换了三个指针的值(以vector为例,vector中的三个指针分别是首元素迭代器,尾后迭代器,指示最多容纳元素的迭代器),自然快得不得了。swap应该也只是交换了指针。
五、标准库容器结构
其中标红的三个是C++11新增加的容器,其中array和forward_list比较简单,unordered_containers比较重要,可惜缺失了很多内容。
1. array
其实里面只是一个C语言的数组,只是包装成了一个class的样子。下面是TR1版本(C++03)的源代码,简洁易懂:
template<typename _Tp, std::size_t _Nm>
struct array
{
typedef _Tp value_type;
typedef _Tp* pointer;
typedef value_type* iterator;
value_type _M_instance[_Nm ? _Nm : 1];
iterator begin()
{ return iterator(&_M_instance[0]); }
iterator end()
{ return iterator(&_M_instance[_Nm]); }
......
};
注意里面并没有构造函数和析构函数。
与TR1版本不同,GCC4.9array的源码相当复杂,不容易读懂,见视频,这里就不截图了。
2. 容器hashtable
注意这部分并不是新东西,老师也只简单介绍了原理:其实就是使用链地址法解决冲突的哈希表,key的计算方式是对哈希表长度取余,哈希表长度是一个素数。如果hash表放不下了,就要进行rehashing,将哈希表扩容为大概原先的两倍,当然扩容以后也得是个素数。
hashtable里面的key虽然是数字,但里面其实什么对象都能放,只是我们需要告诉它hashfunction是什么,我把hashfunction理解为一个怎样讲对象计算为一个数字的函数。当然最好一个对象能够唯一地对应一个数字。
3. hashfunction
(1)标准库的hashfunction的用法
给unordered容器所用的哈希表进行算法处理,将每个元素给予其标号的方法。
上图是标准库已有的hashfunction,可以将一些类型转化为size_t的数值。其中hash<T>代表一个类型,所以hash<T>()就是一个临时对象,并且这是个可调用对象。
(2)标准库的hashfunction定义的逻辑
以GCC2.9的定义为例:
//泛化版本
template <class Key> struct hash { };
//很多特化版本
_STL_TEMPLATE_NULL struct hash<char>{ //_STL_TEMPLATE_NULL就是template<>
size_t operator()(char x) const { return x; }
};
_STL_TEMPLATE_NULL struct hash<short>{
size_t operator()(short x) const { return x; }
};
_STL_TEMPLATE_NULL struct hash<unsigned short>{
size_t operator()(unsigned short x) const { return x; }
};
.......
基本逻辑是定义一个空的泛化版本,然后针对各种类型再定义特化的版本。GCC4.9的实现要复杂得多,并且不仅仅定义了整型的hashfunction,还有指针类型,float,double等等等等,但是结构复杂,具体实现原理也不明,有兴趣可以去看PPT。class string之类也有自己的hashfunction,但是是和string定义在一起的。
下面我们来看一下G4.9的源码:
(3)定义自己的hashfunction------------一个万用的hashfunction(本部分内容缺失,有空对着PPT学一下)
- 形式1:
前面讲哈希表时说过Hash Function,在为整数时即标号为自己,为字符串类型时进行一个逐位运算。有没有一种可以直接万用的Hash Function呢?我们进行如下学习,见下图:
同样的东西,左边是成员函数,而右边是全局函数。
左上角的情况:可以运作,但碰撞很多,太过天真。
而右上角的情况:hash_val根据不同的参数类型,按顺序调用不同的重载的函数(黑色的圈1,圈2,圈3)。其中圈1调用了可变模板参数,逐个处理每一个(见:),一步步走到左下方。
综上,来看一下这个Hash Function的源码:
- 形式2:
六、tuple
将多个类型整合到一起:
不是28而是32的原因,暂无解释。。。
源码如下:
基础:可变参数模板,简单来说就是:分成一个和一堆,把n分为1和n-1,随后继续将n-1分为1和n-2,这样不断递归。代码中体现为...
。详见:
tuple最神秘的地方:有个private继承,继承一部分自己(Tail部分的自己),故可以递归继承,形成上图右边的继承顺序情况。终止条件:递归继承到空的tuple<>。
来看一下tuple的历史:
一、Rvalue_reference(右值引用)和move语义
1、左右值概念区分
(1)左右值概念区分
-
左值:表达式结束后依然存在的对象,我们也叫做变量;可以出现在=左边也可以在右边。
-
右值:表达式结束后就不存在的临时对象;只能出现在=右边;例如临时对象。
(2)判断左值和右值的方法
-
能对表达式取地址的是左值,否则就是右值
-
左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)
(3)左右值举例分析
-
左右值书写分析
注意:
虽然string和复数的举例推翻了左右值书写的准则,但是这是由于一些C++定义类型导致的,我们不要去管他;
我们只要记住两点:1、临时对象就是一个右值;2、右值不要出现在等号左边。
2、右值引用和move语义
右值引用:为了避免非必要的拷贝操作。move类似“浅拷贝”,只是把指针指向另一个位置,可以理解为“偷”,因此不同于copy的“深拷贝”。
右值引用出现在C++03版本之后,它可以避免一些不必要的拷贝和临时对象,当赋值操作的右边是一个右值时,左值可以偷取右值里的资源,而不必去执行allocator(分配内存的动作),这就是move语义。
在C++11之前我们对一个函数返回值取地址是错误的;但是在C++11的新语法中,我们可以使用&&符号表示对右值取引用或者使用move函数将一个左值变为右值;相应的,我们也要为对应的元素对象实现一个move构造函数或者move赋值函数的重载版本(适用容器中操作元素时)。比如在做容器的在C++ 2.0之后,容器的插入动作都提供了一个insert的重载版本,专门适用这种新语法,如下所示:
当编译器检测到我们insert的值是一个右值(move函数返回一个右值)或者右值引用(&&,临时对象都会被当成右值引用)时,会调用下面新增的这个重载函数,让它偷取这个右值的东西免去自身取构造内存的动作,因为插入动作会调用拷贝构造函数,如果插入的元素是一个基本类型而不需要额外提供什么,但是如果插入的是一个复杂类型,原本我们需要写一个拷贝构造函数,开辟一块内存一个个的赋值过去,但现在我们要提供一个move搬移构造就行了(比如像string(编译器已经实现)类,move构造函数只是将既有的内部字符数组赋予新对象就行了,此时相当于新对象指针和原对象指针指向同一个地方,要注意执行move后原对象的指针是个不确定状态,不能使用),所以任何非平凡的类(除了基本类型),都应该提供move构造和move assignment(赋值)函数:
-
这里说一下move中的偷的概念,所谓偷就是借用之前的值,对于指针来说就是两个指针指向同一个地方,也就是说move语义就是指针的浅拷贝,为了指针的安全我们还要在偷完之后将原来的指针打断以禁止后续再使用这个值
-
右值经函数转交到下一个函数时会变成一个左值
3、完美转发
(1)不完美转发
(2)完美转发
6、右值引用使用准则:
使用右值的函数也不能返回局部变量。
7、move类举例
-
move构造函数对接的是拷贝构造函数,下面将两个做对比,move做浅拷贝后,要将原来指针赋空(记住一定要将原来的指针赋空打断,因为容器在做插入操作时产生的临时对象生命周期结束后会调用析构函数释放指针内存,如果原来的指针没有赋空析构后容器里面已插入的指针就是悬空指针,引起崩溃,而前面指针赋空开则不会有影响(对一个空指针delete操作相当于什么都不做))
-
拷贝赋值对接move拷贝赋值函数
8、有无move版本对容器的影响
-
vector插入元素时影响很大
-
除vector以外的其他容器插入元素时影响都不大
-
deque的极端情况,比如每次都在buffer中间插入,会产生很多的拷贝动作,影响也大
二、新的容器
这部分在前面STL中已经学习过。
给unordered容器所用的哈希表进行算法处理,将每个元素给予其标号的方法。
实现方式:
下面我们来看一下G4.9的源码:
-
形式1:
前面讲哈希表时说过Hash Function,在为整数时即标号为自己,为字符串类型时进行一个逐位运算。有没有一种可以直接万用的Hash Function呢?我们进行如下学习,见下图:
同样的东西,左边是成员函数,而右边是全局函数。
左上角的情况:可以运作,但碰撞很多,太过天真。
而右上角的情况:hash_val根据不同的参数类型,按顺序调用不同的重载的函数(黑色的圈1,圈2,圈3)。其中圈1调用了可变模板参数,逐个处理每一个(见:),一步步走到左下方。
综上,来看一下这个Hash Function的源码:
-
形式2:
3.3 tuple
将多个类型整合到一起:
不是28而是32的原因,暂无解释。。。
源码如下:
基础:可变参数模板,简单来说就是:分成一个和一堆,把n分为1和n-1,随后继续将n-1分为1和n-2,这样不断递归。代码中体现为...
。详见:
tuple最神秘的地方:有个private继承,继承一部分自己(Tail部分的自己),故可以递归继承,形成上图右边的继承顺序情况。终止条件:递归继承到空的tuple<>。
来看一下tuple的历史:
作者:灼光
出处:灼光 - 博客园
本博客文章大多为原创,转载请在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。