STL标准库与泛型编程(侯捷)
本文是学习笔记,仅供个人学习使用。如有侵权,请联系删除。
参考链接
Youbute: 侯捷-STL标准库与泛型编程
B站: 侯捷 - STL
Github:STL源码剖析中源码 https://github.com/SilverMaple/STLSourceCodeNote/tree/master
Github:课程ppt和源码 https://github.com/ZachL1/Bilibili-plus
文章目录
- STL标准库与泛型编程(侯捷)
- 40 一个万用的hash function
- 41 Tuple 用例
- 42 type traits
- 43 type traits 实现
- 44 cout
- 45 movable元素对于不同容器速度效能的影响
- 46 测试函数
- 后记
下面是C++标准库体系结构与内核分析的第四讲笔记,也是这门课的最后一篇笔记。
主要包括tuple, type traits的介绍,移动构造等。
截至2024年1月9日,花费5天的时间,马不停蹄地结束《STL标准库与泛型编程》这门课。
40 一个万用的hash function
使用hashtable的容器的时候,可以设计一个hash function,下图是两种模式,一种是设计成成员函数,另一种是一般的函数。
具体怎么做,下图是几种方法。
左上角是naive approach:直接把属性的所有hash值加起来,这种方法在hashtable中会产生很多的碰撞,放在同一个bucket中的元素会多。
另一种做法是使用可变模板参数来做的hash_val()函数,产生种子,经过复杂的操作,得到hash值。
还有一种方法是利用struct hash的偏特化来实现hash function
41 Tuple 用例
std::tuple
是 C++ 标准库中的一个模板类,用于组织多个元素(值或者引用)为一个单一的对象。std::tuple
提供了一个元组(tuple)的概念,类似于一个固定大小的、不同类型的数组。
以下是一些关键的特性和用法:
-
组织多个元素:
std::tuple
可以包含零个或多个元素,每个元素可以是不同类型的。 -
元素的访问: 可以使用
std::get
函数或结构化绑定(C++17 及以上)来访问元组中的元素。例如:#include <tuple> #include <iostream> int main() { // 创建一个包含整数、浮点数和字符串的元组 std::tuple<int, float, std::string> myTuple(42, 3.14f, "Hello"); // 使用 std::get 访问元组中的元素 std::cout << "First element: " << std::get<0>(myTuple) << std::endl; std::cout << "Second element: " << std::get<1>(myTuple) << std::endl; std::cout << "Third element: " << std::get<2>(myTuple) << std::endl; return 0; }
-
结构化绑定(C++17 及以上): 可以使用结构化绑定直接将元组的元素绑定到变量,使得代码更加清晰:
#include <tuple> #include <iostream> int main() { // 创建一个包含整数、浮点数和字符串的元组 std::tuple<int, float, std::string> myTuple(42, 3.14f, "Hello"); // 使用结构化绑定访问元组中的元素 auto [first, second, third] = myTuple; std::cout << "First element: " << first << std::endl; std::cout << "Second element: " << second << std::endl; std::cout << "Third element: " << third << std::endl; return 0; }
-
元组的比较:
std::tuple
支持比较操作,可以用于按照字典序比较元组。 -
元组的拆包: 可以使用
std::make_tuple
创建元组,也可以使用std::tie
将元组的值绑定到变量,方便进行函数的多返回值:#include <tuple> #include <iostream> std::tuple<int, double, std::string> getValues() { return std::make_tuple(42, 3.14, "Hello"); } int main() { int intValue; double doubleValue; std::string stringValue; // 使用 std::tie 拆包 std::tie(intValue, doubleValue, stringValue) = getValues(); std::cout << "Int value: " << intValue << std::endl; std::cout << "Double value: " << doubleValue << std::endl; std::cout << "String value: " << stringValue << std::endl; return 0; }
总体而言,std::tuple
提供了一种方便的方式来组织和处理多个元素,尤其在函数返回多个值的场景中使用较为方便。
tuple<Head, Tail…> 继承 tuple<Tail…>
这样的递归定义使得 tuple
类模板可以方便地处理可变数量的模板参数,每一层递归处理一个参数。这也是元编程中常见的技术,通过递归和继承来处理可变数量的参数。
42 type traits
在C++中,type_traits
是一种元编程技术,用于在编译时判断和查询类型的特性。它通常包含了一系列的嵌套类型成员(type members)或者常量值成员(value members),用于描述和查询类型的特性。这些特性包括是否有默认构造函数、是否是 POD(Plain Old Data)、是否具有特定的特性等等。
struct __true_type {
};
struct __false_type {
};
template <class _Tp>
struct __type_traits {
typedef __true_type this_dummy_member_must_be_first;
/* Do not remove this member. It informs a compiler which
automatically specializes __type_traits that this
__type_traits template is special. It just makes sure that
things work if an implementation is using a template
called __type_traits for something unrelated. */
/* The following restrictions should be observed for the sake of
compilers which automatically produce type specific specializations
of this class:
- You may reorder the members below if you wish
- You may remove any of the members below if you wish
- You must not rename members without making the corresponding
name change in the compiler
- Members you add will be treated like regular members unless
you add the appropriate support in the compiler. */
typedef __false_type has_trivial_default_constructor;
typedef __false_type has_trivial_copy_constructor;
typedef __false_type has_trivial_assignment_operator;
typedef __false_type has_trivial_destructor;
typedef __false_type is_POD_type;
};
// Provide some specializations. This is harmless for compilers that
// have built-in __types_traits support, and essential for compilers
// that don't.
#ifndef __STL_NO_BOOL
__STL_TEMPLATE_NULL struct __type_traits<bool> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type;
};
#endif /* __STL_NO_BOOL */
__STL_TEMPLATE_NULL struct __type_traits<char> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type;
};
__STL_TEMPLATE_NULL struct __type_traits<signed char> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type;
};
type traits特化
type traits测试
string类里面没有虚析构函数:
在C++中,std::string
类的析构函数并不是虚析构函数。这是因为 std::string
类通常不被设计为作为基类使用,而是用作独立的、不涉及多态性的类。虚析构函数主要用于在继承关系中的基类,以确保正确调用派生类的析构函数。
由于 std::string
通常不作为基类使用,因此它的析构函数不需要是虚的。虚函数会引入额外的开销,包括虚函数表(vtable)的维护,而对于非多态的类来说,这是不必要的。
如果你需要在继承体系中使用多态性,可能会使用指向基类的指针或引用来操作派生类对象。在这种情况下,基类应该有虚析构函数,以确保正确调用派生类的析构函数。然而,对于 std::string
这样的类,通常不需要在继承体系中使用多态性,因此它的析构函数没有被声明为虚函数。
这里侯捷老师提到虚析构函数,这里复习一下:
虚析构函数(Virtual Destructor)是 C++ 中的一个概念,通常用于处理基类指针指向派生类对象时的正确析构行为。
当一个类中包含虚函数时,通常都应该声明一个虚析构函数。虚析构函数的声明形式如下:
class Base {
public:
virtual ~Base() {
// 虚析构函数的实现
}
// 其他成员函数和数据成员...
};
在上述代码中,~Base()
是虚析构函数。虚析构函数通过关键字 virtual
进行声明,这样派生类就可以选择性地覆盖它。这样一来,当使用基类指针指向派生类对象,并通过这个指针删除对象时,将会调用适当的派生类析构函数,确保对象的正确清理。
例如:
class Derived : public Base {
public:
~Derived() override {
// 派生类的析构函数实现
}
// 其他成员函数和数据成员...
};
int main() {
Base* ptr = new Derived();
// 使用基类指针删除对象,调用的是派生类的析构函数
delete ptr;
return 0;
}
在这个例子中,通过基类指针 Base* ptr
删除一个 Derived
类型的对象时,由于基类析构函数是虚函数,将调用 Derived
类的析构函数。这样确保了在多态(polymorphic)情况下正确释放资源。
测试虚析构函数,type traits能否正确显示出来
下面用到了C++11的&&语法:
在C++11及之后的标准中,&&
是右值引用(Rvalue Reference)的语法。
右值引用是一种引用类型,用于表示对右值(如临时对象、将要销毁的对象等)的引用。在C++11中,引入了右值引用的概念,通过 &&
来声明右值引用。右值引用的主要特点是能够绑定到临时对象,而传统的左值引用(&
)主要用于绑定到可修改的左值。
以下是右值引用的基本语法:
T&& variable_name; // T 是某种类型,variable_name 是变量名
其中,T
是被引用的类型。右值引用主要用于优化资源管理和实现移动语义,其中移动语义可以避免不必要的内存拷贝,提高程序性能。
一个常见的例子是移动构造函数和移动赋值运算符的使用,它们使用右值引用来实现对资源的高效转移。例如:
class MyClass {
public:
// 移动构造函数
MyClass(MyClass&& other) {
// 实现资源的移动
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) {
// 实现资源的移动
return *this;
}
};
// 使用右值引用创建对象
MyClass obj1;
MyClass obj2 = std::move(obj1); // 使用 std::move 将左值转为右值
在上述例子中,std::move
函数用于将左值转为右值,这样可以调用移动构造函数或移动赋值运算符,从而实现高效的资源管理。Move constructor(移动构造函数)是C++11引入的一种构造函数,用于实现对象的资源转移,以提高程序的性能。它允许在不复制资源的情况下将对象的内容从一个对象转移到另一个对象。移动构造函数使用右值引用(Rvalue Reference)来实现。
43 type traits 实现
type traits实现 is_void
模板类的设计方法,有一个泛化版本,后面跟着特化版本。
下面的remove_const的特化,类型是_Tp const ,这是范围上的偏特化。
remove_volatile也是同样的操作。
remove_cv是调用remove_const和remove_volatile。
44 cout
cout
是 C++ 标准库中的输出流对象,用于将数据输出到标准输出设备,通常是控制台。它是 ostream
类的一个实例,是 C++ 中常用的输出工具之一。
一个东西想要能够丢给cout,就是对<<操作符的重载
下面就是各种类对<<操作符的重载,以实现在标准输出设备上输出。
45 movable元素对于不同容器速度效能的影响
测试三百万个元素放入不同的容器
movable元素对vector速度的影响
这里CCtor和MCtor都是构造函数的调用次数,由于vector的底层会两倍空间扩展,在扩展的时候会调用拷贝构造,所以这里调用次数为7194303,明显大于三百万。
movable元素对list速度的影响
movable元素对deque速度的影响
movable元素对multiset速度的影响
movable元素对unordered_multiset速度的影响
写一个moveable class
move constructor是用一个指针指向资源,避免资源的深度复制。
move assignment
46 测试函数
下图是对上面移动构造的时间开销的测试
vector的copy constructor
深拷贝:耗费大量时间
vector的move constructor
string时候movable呢?
如下图所示,string带有movable的功能,从&&右值可以看出。
后记
这是STL标准库与泛型编程的最后一篇笔记,这门课完结。
截至2024年1月9日,花费5天的时间,马不停蹄地结束《STL标准库与泛型编程》这门课。