文章目录
C++STL学习第四讲(讲解STL周边内容)
1. 一个万用的 Hash Function(hash_val)
1. 大致思路
- 将 object 分解为基本单元(数值和字符串),使用已有的基本单元的 Hash Function 构建 object 的 Hash Function;
2. Hash Function 实现形式
-
① function object;
-
② 一般 function:
- 使用时,模板参数需要传入 function pointer(图中的 function pointer 没有名称);
- 构造参数需要传入 function pointer 指定的 function 名称;
-
③ 在 std namespace 下特化 struct hash:
-
unordered_set、unordered_map 等以 hashtable 为底层实现的 container,第二参数(hash function)的缺省值是 hash;
-
特化 struct hash 示例:
-
namespace std // 在namespace std下 { // 模仿已有的hash样式 template<> struct hash<object> { size_t operator()(cosnt object&) const noexcept{ ... } }; }
-
-
G4.9 所有的基本类型都有 hash 特化版本;
-
3. 用例介绍
-
假设 Customer 类有三个 data:
- string fname;
- string lname;
- long no;
-
示例中使用 function object 实现 hash function;
-
单纯地相加基本单元的 hash code:
- 分别使用 std::hash 计算三个 data 的 hash code,然后相加作为 Customer 的 Hash Function,根据经验得到,这种做法容易产生碰撞,导致 hashtable 中的链表变长,不利于查找;
- 分别使用 std::hash 计算三个 data 的 hash code,然后相加作为 Customer 的 Hash Function,根据经验得到,这种做法容易产生碰撞,导致 hashtable 中的链表变长,不利于查找;
4. hash_val 介绍
-
hash_val 是从 TR1(C++98和C++11之间的过渡版本)版本开始使用的;
-
hash_val 使用了可变模板参数 typename… T(C++11);
- 【对可变模板参数的处理,一般是递归进行拆分,每次都拆分成 1 + (n - 1) 个;】
-
hash_val 有三个重载版本:
- 三个版本因为参数类型不同或者参数个数不同,所以可以进行重载;
-
hash_val 调用过程:
-
// 示例代码 return hash_val(c.fname, c.lname, c.no);
-
① 先调用 hash_val 的第 1 版本,因为第一参数不符合第 2、3 版本;
- 初始化种子 seed,继续调用 hash_val;
-
② 调用 hash_val 的第 2 版本;
- 第 2 版本将可变参数拆分成 1 + (n - 1) 个;
- 将 seed 、拆分的第一个参数作为函数参数,调用 hash_combine,更新 seed;
- 递归调用自己(以 seed、剩下的参数作为函数参数);
-
③ 当第 2 版本拆分成 1 + 1 个参数,这时调用 hash_val 的第 3 版本(arg… 只剩一个参数);
- 调用最后一次 hash_combine;
-
④ 返回的 seed 就是得到的 hash code;
-
-
2. tuple 元组
1. tuple 介绍
- tuple 在 TR1 版本加入;
- tuple 是一组有序对象的集合,tuple 可以存放任意类型(即 tuple 中各个 object 的类型可以不同);
2. tuple 使用
1. 初始化
-
// 1.默认构造 tuple<string, int, int, complex<double>> t; // 2.带参构造 tuple<int, float, string> t1(41, 6.3, "nico"); // 3.借助make_tuple和auto,不需要指定类型 auto t2 = make_tuple(22, 44, "stacy");
2. 获取内容
-
// 1.获取tuple对应位置的内容,不能越界!!! int p = get<0>(t1); get<0>(t1); get<1>(t1); // 2.可以重新赋值tuple的内容 get<1>(t1) = get<1>(t2); // 类型要对应或者可以隐式转换
3. 重载函数
-
// 1.重载operator<() if (t1 < t2) { ... } // 2.拷贝赋值 t1 = t2; // 3.cout重载了operator<<(tuple)版本 cout << t1;
4. 集体赋值
-
tuple<int, float, string> t3(77, 1.1, "more light"); int i1; float f1; string s1; // 1.借助tie,使用tuple的内容进行集体赋值 tie(i1, f1, s1) = t3;
5. 获取 tuple 的属性信息
-
typedef tuple<int, float, string> TupleType; // 1.借助tuple_size<TupleType>::value获取tuple的元素个数 tuple_size<TupleType>::value; // 3 // 2.tuple_element<index, TupleType>::type,获取tuple对应index的元素类型 tuple_element<1, TupleType>::type f1 = 3.3; typedef tuple_element<1, TupleType>::type t1dd;
3. tuple 源码
1. 关键部分
-
tuple 使用了可变模板参数 typename… T(C++11);
- 通过继承实现递归拆分 Tail…,形成一组继承关系;
-
tuple 有一个泛化版本,一个特化版本:
-
泛化版本:
-
template<typename Head, typename... Tail> class tuple<Head, Tail...> : private tuple<Tail...> { ... };
-
-
特化版本作为拆分终止条件:
-
// 模板参数和class内容为空 template<typename... Values> class tuple<> {};
-
-
2. tuple 内容
-
data:
-
Head m_head :记录可变参数的第一个参数;
-
typedef:
- typdef tuple<Tail…> inherited;
-
function:
-
// 返回tuple的第一个元素 typename Head::type head() {return m_head;} /* 由于在继承关系中,先构造父类数据,再构造子类数据; 所以在内存中,父类的数据 */ inherited& tail() {return *this;}
-
3. 继承过程
-
示例 tuple:
-
tupe<int, float, string> t(41, 6.3, "nino");
-
-
继承过程【通过继承递归划分】:
- ① 开始调用了 tupe<int, Tail…>,head() 返回 41;
- Tail… 包含 float 和 string;
- ② 第①步继承了 tuple<Tail…>,实际调用了 tupe<float, Tail…>,head() 返回 6.3;
- Tail… 包含 string;
- ③ 第②步继承了 tuple<Tail…>,实际调用了 tupe<string, Tail…>,head() 返回 “nino”;
- Tail… 为空;
- ④ 第③步继承了 tuple<Tail…>,实际调用了 tupe<>(拆分终止条件);
- ① 开始调用了 tupe<int, Tail…>,head() 返回 41;
4. tail() function 讲解
- 预备知识:
- 在继承关系中,会先调用父类的构造函数,再调用子类的构造函数;
- 因此在内存中,会先存储父类的变量成员,再保存子类的变量成员(和构造顺序相同);
- 以上述的示例 tuple 为例:
- 继承关系如图右边所示;
- 因此 tuple 的数据在内存中的分布,如图下方所示;
- 从上往下依次是:string,float,int(倒序);
- inherited& tail() { return *this; }:
- 根据父类和子类的 class member 在内存中的分布,可以得知,子类和继承的父类的地址指向位置是相同的(图中的圆圈位置);
- tail() function 返回自身 *this,且返回类型为继承的父类;
- 【将子类 object 转型为 父类 object,会发生内容截断;】
- 当 tuple<int, float, string> 调用 tail(),会返回继承的父类 tuple<float, string>;
- tuple<float, string> 在内存中的范围如图深灰色方框所示;
5. 注意点
- 实际使用没有必要调用 head 和 tail 来获取 tuple 内容;
- 之前的 tuple 使用小结中,有获取内容的辅助函数;
- 之前的 tuple 使用小结中,有获取内容的辅助函数;
3. type traits
1. G2.9 版 type traits
- G2.9 版 type traits 包含 5 个 associated types(第一个没用);
- has_trivial_default_constructor;
- has_trivial_copy_constructor;
- has_trivial_default_constructor;
- has_trivial_assignment_constructor;
- is_POD_type;
- POD == Plain Old Data 平淡的旧数据,即只有 data,没有 function 的 class / struct;
- __true_type 和 __false_type 是两个空类,这和 iterator_category 的五个类型是一样的;
- G2.9 type traits 为每个基本类型都进行了特化;
- 对于自定义的 class,需要自己特化 type traits,根据自己的理解设置 ture or false;
- 这种使用方式不够方便;
- 这种使用方式不够方便;
2. C++11 的 type traits
-
C++11 为每个 type trait 属性都单独定义了一个 template class,而不是集中放在 type traits 中;
-
使用时直接将 class object 传入某个 type trait,编译器就会自动根据传入 class 的内容判断 true or false(很强大);
-
C++11的 type traits 图示:
3. C++11 type traits 使用
1. 使用形式
- [type name]::value,返回 0 或 1;
2. Foo class(is POD)
- Foo 只有 data,所以 Foo is_pod == 1;
3. Goo class(has virtual function)
- is_polymorphic:是多态吗;
- 多态类指:声明 or 继承一个 virtual function 的 类;
- 多态类指:声明 or 继承一个 virtual function 的 类;
4. Zoo class(涉及 C++11 新语法)
- = delete:C++11 新语法,表示这个 member function 不能被调用;
- = default:C++11 新语法,表示创建默认成员函数(只能用于特殊的成员函数,构造、析构等);
- &&:C++11 新语法,和 move 操作相关:
- move 构造;
- move 赋值;
4. C++11 type traits 实现
1. is_void
- ① 获取 remove_cv<_Tp>::type(去除关键字):
- 先获取 remove_volatile<_Tp>::type,在这基础上获取 remove_const<_Tp>::type;
- 【即传入 _Tp,获取去除 volatile 和 const 关键字的 type;】
- remove_volatile 有一个泛化版本,一个偏特化版本:
- 泛化版本直接返回 _Tp;
- 偏特化版本针对模板参数为 _Tp volatile 的情况,返回 _Tp(去除 volatile 关键字);
- remove_const 有一个泛化版本,一个偏特化版本:
- 泛化版本直接返回 _Tp;
- 偏特化版本针对模板参数为 _Tp const 的情况,返回 _Tp(去除 const 关键字);
- ② 将 remove_cv<_Tp>::type 传入 is_void_helper<>,获取 type;
- is_void_helper 有一个泛化版本,一个偏特化版本:
- 泛化版本,继承 false_type class;
- 偏特化版本针对模板参数为 void 的情况,继承 true_type class;
- 【false_type 和 true_type 中有一个 value 成员变量,为 bool 类型;】
- is_void_helper 有一个泛化版本,一个偏特化版本:
2. is_integral
- ① 获取 remove_cv<_Tp>::type(去除关键字):
- 先获取 remove_volatile<_Tp>::type,在这基础上获取 remove_const<_Tp>::type;
- 【即传入 Tp,获取去除 volatile 和 const 关键字的 type;】
- ② 将 remove_cv<_Tp>::type 传入 is_integral_helper<>,获取 type;
- is_integral_helper 有一个泛化版本,多个全特化版本:
- 泛化版本,继承 false_type class;
- 全特化版本针对所有数值类型,继承 true_type class;
- 【false_type 和 true_type 中有一个 value 成员遍历,为 bool 类型;】
- is_integral_helper 有一个泛化版本,多个全特化版本:
3. 涉及 class 内部信息的 type traits
- 图中蓝色部分在 C++ 标准库源代码找不到;
- 可以猜测,这些判断 class 内部信息的工作,都交给了编译器,因为编译器在编译时肯定知道 class 内部的所有信息;
4. cout
1. cout 简单介绍
- cout 是一个 object;
- cout 的 class 是 ostream;
- 在 iostream 头文件中有一个 extern object,命名为 cout,使其他用户可以直接使用 cout;
- extern 关键字:
- 修饰符 extern 用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
- extern声明不是定义,即不分配存储空间;
- extern 实现了声明和定义分离;
- ostream 重载了所有基本类型的 cout << 操作符;
2. G4.9版的 cout
- G4.9版的 cout 又重载了很多常用类型的 << 操作符;
3. 重载 cout
- 如果想要使用 cout 输出自定义的 class,需要重载一个 operator<<()函数:
- 一般 operator<<() 函数 是全局函数,然后在 class 中将 operator<<() 设置为 friend function;
- 一般 operator<<() 函数 是全局函数,然后在 class 中将 operator<<() 设置为 friend function;
5. 简单介绍 move
1. move 的实质
-
对于带有 pointer 的 class:
- copy 就是深拷贝;
- move 类似于浅拷贝,在浅拷贝的基础上,断开源指针的连接;
-
【当传入参数是一个右值,编译器会自动调用 move 版本的函数(如果 class 有实现的话);】
-
使用 move 构造或者 move 赋值后,源数据(带有 pointer 的 class)作废,不能再使用;
2. 实现一个 moveable class
-
关键部分,实现 move 构造和 move 赋值;
- move-ctor:
- 使用 initialization list(构造函数都应该用列表初始化),初始化 data(包括 pointer);
- 断开源数据的 pointer 连接;
- move-asgn:
- 和 move-ctor 思路相同,类似浅拷贝操作;
- 析构函数:
- 先判断 pointer 是否为空指针,然后再手动 delete;
- 对于带有 pointer 的 class,同时实现了 move 相关操作,一定要这么写;
- 避免一个 pointer 被 delete 两次,发生一些未知的情况;
- 没有实现 move 操作,也可以养成习惯;
- move-ctor:
-
【移动构造和拷贝赋值同时存在(移动赋值和拷贝赋值),调用哪个???】
3. moveable class 对容器的insert速度效率影响
- 容器的内容都是 string(带有指针的 class);
1. vector
- 对于 vector 这种扩容需要重新拷贝数据的容器:
- 使用 move 操作,速度效率能提升很大;
- 使用 move 操作,速度效率能提升很大;
2. 其他容器
- 对于 list、deque、multiset、unordered_multiset 容器:
- move 操作更快,但和 copy 操作不会差很多;
4. 测试程序
- 通过 iterator_traits 获取容器的 value_type;
- 测试 moveable:
- 往容器中插入 300000 个字符串(计算时间);
- 对于 moveable class:
- 由于 insert 传入的是右值 object,编译器会自动调用 move 构造或者 move 赋值;
- 对于 moveable class:
- 往容器中插入 300000 个字符串(计算时间);
- 对容器进行 copy 构造(计算时间);
- 对容器进行 move 构造(计算时间);
- 使用 std::move 强制将左值转为右值;
- 对容器进行 swap(计算时间);
- 测试 non-moveable:
- …
- …
5. vector 的 move ctor
- vector 的 move 构造只是交换三个 pointer(图右);
6. string 是否是 moveable
- string 有 move 构造和 move 赋值;