谨以此记录MyTinySTL项目的学习
STL(Standard Template Library)标准模板库,是C++标准库的一部分,它提供了一组通用的模板类和函数,用于处理常见的数据结构和算法。STL的设计目标是为C++程序员提供高效、可复用的数据结构和算法,以提高代码的可维护性和性能。
从代码随想录中了解到MyTinySTL这个项目,从原作者的Github那下下来后打开一看,一头雾水。Carl哥说的好,要学习一个项目,不要一上来就阅读源码,把项目跑通是首要任务。要想运行这个项目,直奔test文件夹下的test.cpp文件,运行它即可。用VS2017运行这个项目时可能会遇到报错,解决BUG的方法也很简单,如果显示语法错误的话可以把显示语法错误那行代码附近的中文注释删掉即可。
代码运行成功后,就可以仔细学习这个项目了。我个人的学习方式是按照下表(原表地址:参考博客-1
)中的文件层级读明白每一个文件中的源码,并找出它们之间的联系,这会是一个漫长的过程,我也不清楚这是否是高效的学习方式,但是是我目前所能想到的最佳方式。
层级 | 类 |
第八层 | unordered_map.h、unordered_set.h |
第三层 | algobase.h、construct.h、heap_algo.h、numeric.h、set_algo.h |
第二层 | iterator.h、util.h |
第一层 | exceptdef.h、functional.h、type_traits.h |
底层 | cstddef、ctime、cstring、new、iostream、initializer_list、stdexcept、cassert、cstdlib、climits、type_traits、 |
上表对MyTinySTL项目中两大文件夹test、include下的各个文件进行分级汇总,按照从底层到顶层的学习顺序便于理解整个项目。
一、底层
底层中的元素不是项目里的文件,而是文件中用到的各个头文件。建议在阅读源码时遇到头文件再去查它的作用。
二、第一层
- exceptdef.h
位于include文件夹下,目的是提供一些用于错误处理和调试的宏和异常抛出工具。宏(Macro):是一种在编程中用于进行代码替换的预处理指令,常用#define定义。
#ifndef MYTINYSTL_EXCEPTDEF_H_
#define MYTINYSTL_EXCEPTDEF_H_
#include <stdexcept>
#include <cassert>
namespace mystl
{
#define MYSTL_DEBUG(expr) \
assert(expr)
#define THROW_LENGTH_ERROR_IF(expr, what) \
if ((expr)) throw std::length_error(what)
#define THROW_OUT_OF_RANGE_IF(expr, what) \
if ((expr)) throw std::out_of_range(what)
#define THROW_RUNTIME_ERROR_IF(expr, what) \
if ((expr)) throw std::runtime_error(what)
}
#endif
整个代码块被包裹在 #ifndef MYTINYSTL_EXCEPTDEF_H_和#endif之间的条件编译指令中。这是为了防止多次包含同一个头文件,以避免重复定义(C++的防卫式声明)。
MYSTL_DEBUG(expr):用于进行断言检查,类似于<cassert>中的assert宏。如果expr为假,它将触发断言失败并终止程序。
THROW_LENGTH_ERROR_IF(expr, what):检查容器或序列长度是否超出了预期范围。
THROW_OUT_OF_RANGE_IF(expr, what):检查访问容器或序列时的索引是否越界。
THROW_RUNTIME_ERROR_IF(expr, what):其他异常(如超时异常)。
总而言之,这部分代码在开发、调试过程中提供一些工具来检测和处理潜在的问题,提高代码的健壮性。
- functional.h
位于include文件夹下,是STL中的重难点:仿函数(函数对象)
仿函数的分类:
以操作符划分:分为一元仿函数与二元仿函数(没有其他的了)
以功能划分:分为算术运算、关系运算、逻辑运算
代码中首先定义了两个模板结构体unarg_function和binary_function,用于定义一元函数和二元函数的参数类型和返回值类型。
一、一元仿函数和二元仿函数
Unarg_function
其定义如下,一旦某个仿函数继承了unarg_function,其用户就可以取得该仿函数的参数类型,或其返回值类型。
// 定义一元函数的参数型别和返回值型别
template <class Arg, class Result>
struct unarg_function
{
typedef Arg argument_type;
typedef Result result_type;
};
对unarg_function而言, Arg是函数的参数类型, Result是函数的返回值类型。而{}内则是对 Arg和Result设置别名,这意味着在模板结构体unarg_function中,你可以使用argument_type来引用类型Arg。
binary_function
其定义如下,一旦某个仿函数继承了binary_function,其用户就可以取得该仿函数的参数类型,或其返回值类型。
对于binary_function而言,Arg1, Arg2, Result分别表示函数中第一个参数的参数类型,第二个参数的参数类型,以及二元函数的返回值类型。{}内的内容与一元函数类似。
二、算数类(Arithmetic)仿函数
STL内建的“算术类仿函数”,支持加法、减法、乘法、除法、模数(余数)、和否定运算,除了“否定”运算为一元运算,其余都是二元运算。
包含如下:
加法:plus<T>
减法:minus<T>
乘法:multiplies<T>
除法:divides<T>
模取:modulus<T>
否定:negate<T>
三、关系运算类(Relational)仿函数
STL内建的“关系运算类仿函数”,支持等于、不等于、大于、大于等于、小于、小于等于六种运算。每一个都是二元运算。
包含如下:
等于:equal_to<T>
不等于:not_equal_to<T>
大于:greater<T>
小于:less<T>
大于等于:greater_equal<T>
小于等于less_equal<T>
四、逻辑运算类(Logical)仿函数
STL内建的“逻辑运算类仿函数”,支持逻辑运算中的And、Or、Not三种运算,其中And和Or为二元运算,Not为一元运算。
包含如下:
逻辑与:logical_and<T>
逻辑或:logical_or<T>
逻辑非:logical_not<T>
五、证同(identity)、选择(select)、映射(project)
下面介绍的这些仿函数,都只是将其参数原封不动地传回。其中某些仿函数对传回的参数有刻意的选择,或者可以忽略。
identity
// 证同函数:不会改变元素,返回本身
template <class T>
struct identity :public unarg_function<T, bool>
{
const T& operator()(const T& x) const { return x; }
};
select
// 选择函数:接受一个 pair,返回第一个元素
template <class Pair>
struct selectfirst :public unarg_function<Pair, typename Pair::first_type>
{
const typename Pair::first_type& operator()(const Pair& x) const
{
return x.first;
}
};
// 选择函数:接受一个 pair,返回第二个元素
template <class Pair>
struct selectsecond :public unarg_function<Pair, typename Pair::second_type>
{
const typename Pair::second_type& operator()(const Pair& x) const
{
return x.second;
}
};
project
// 投射函数:返回第一参数
template <class Arg1, class Arg2>
struct projectfirst :public binary_function<Arg1, Arg2, Arg1>
{
Arg1 operator()(const Arg1& x, const Arg2&) const { return x; }
};
// 投射函数:返回第二参数
template <class Arg1, class Arg2>
struct projectsecond :public binary_function<Arg1, Arg2, Arg1>
{
Arg2 operator()(const Arg1&, const Arg2& y) const { return y; }
};
六、哈希函数(hash function)对象
在functional.h的最后部分,定义了一个通用的哈希函数模板hash,并强调了针对指针和整数的特化版本。
针对指针的特化版本:
template <class T>
struct hash<T*>
{
size_t operator()(T* p) const noexcept
{ return reinterpret_cast<size_t>(p); }
};
size_t operaor()*(T* p) 这是一个函数调用运算符的实现,这个函数接受一个指向类型T的指针p作为输入参数,并返回一个size_t类型的哈希值。
reinterpret_cast表示对指针进行类型转换,它可以用来在不进行任何数据转换的情况下改变指针的类型。这里的目的是将指针的位模式转换为整数,以生成哈希值。
const noexcept表示函数是一个常量成员函数并且在运行时不会引发异常。这对于哈希函数是一种良好的属性,因为哈希函数应该是高效和可预测的。
针对整数的特化版本:
#define MYSTL_TRIVIAL_HASH_FCN(Type) \
template <> struct hash<Type> \
{ \
size_t operator()(Type val) const noexcept \
{ return static_cast<size_t>(val); } \
};
可以从代码中看到 static_cast<size_t>(val),表示哈希函数只是返回输入值本身,因为整型类型通常可以直接用作哈希值。
inline size_t bitwise_hash(const unsigned char* first, size_t count)
{
#if (_MSC_VER && _WIN64) || ((__GNUC__ || __clang__) &&__SIZEOF_POINTER__ == 8)
const size_t fnv_offset = 14695981039346656037ull;
const size_t fnv_prime = 1099511628211ull;
#else
const size_t fnv_offset = 2166136261u;
const size_t fnv_prime = 16777619u;
#endif
size_t result = fnv_offset;
for (size_t i = 0; i < count; ++i)
{
result ^= (size_t)first[i];
result *= fnv_prime;
}
return result;
}
这段代码比较抽象,总之,它实现了一种基于 FNV-1a (Fowler-Noll-Vo)哈希算法的哈希函数,可以用于生成输入数据块的哈希值。
对于浮点数float、double、long double进行逐位哈希。用float举例:
template <>
struct hash<float>
{
size_t operator()(const float& val)
{
return val == 0.0f ? 0 : bitwise_hash((const unsigned char*)&val, sizeof(float));
}
};
return val == 0.0f ? 0 : bitwise_hash((const unsigned char*)&val, sizeof(float)):首先,它检查输入的浮点数val是否等于 0.0f。如果等于 0.0f,它直接返回哈希值 0,因为 0 是一个特殊的情况,可以简单地用 0 来表示。如果不等于 0.0f,则调用bitwise_hash函数来计算浮点数val的哈希值。这里将浮点数的位模式转换为字节数组,并调用bitwise_hash函数来计算哈希值。
- type_traits.h
位于include.h文件夹下,用于提供类型信息的类型特征(type traits)。它定义了一些用于提取类型信息的工具,以及一个用于检查类型是否为std::pair的类型特征。
#ifndef MYTINYSTL_TYPE_TRAITS_H_
#define MYTINYSTL_TYPE_TRAITS_H_
// 这个头文件用于提取类型信息
// use standard header for type_traits
#include <type_traits>
namespace mystl
{
// helper struct
template <class T, T v>
struct m_integral_constant
{
static constexpr T value = v;
};
template <bool b>
using m_bool_constant = m_integral_constant<bool, b>;
typedef m_bool_constant<true> m_true_type;
typedef m_bool_constant<false> m_false_type;
static constexpr T value = v:定义了一个静态constexpr成员变量value,它存储了常量值v。
using m_bool_constant = m_integral_constant<bool, b>是m_bool_constant用m_integral_constant创建一个布尔类型的常量类型特征。并为m_bool_constant<true>和m_bool_constant<false>设置别名。
// type traits
// is_pair
// --- forward declaration begin
template <class T1, class T2>
struct pair;
// --- forward declaration end
template <class T>
struct is_pair : mystl::m_false_type {};
template <class T1, class T2>
struct is_pair<mystl::pair<T1, T2>> : mystl::m_true_type {};
} // namespace mystl
#endif // !MYTINYSTL_TYPE_TRAITS_H_
要读懂上面的代码,需要知道C++中的pair模板类。pair用于存储两个不同类型的值(成员)作为一个对。它通常用于需要将两个值一起存储或返回的情况,例如,将两个值打包为一个单元来传递或返回。
代码首先定义了一个pair结构,T1、T2表示两个不同类型的参数。而is_pair用于检查一个类型是否为std::pair类型,默认情况下,is_pair被定义为mystl::m_false_type,表示不是std::pair类型。
当传入的类型是mystl::pair<T1, T2>(std::pair的模板实例)时,is_pair会被特化为 mystl::m_true_type,表示是std::pair类型。
三、第三层
- iterator.h
STL设计的中心思想是将容器(container)和算法(algorithm)分开,迭代器是容器(container)和算法(algorithm)之间的桥梁。
指针可以用来遍历存储空间连续的数据结构,但是对于存储空间非连续的,就需要寻找一个行为类似指针的类,来对存储空间非连续的数据结构进行遍历。因此,引入迭代器概念。
迭代器可以如下定义:提供一种方法,能够依序寻访某个容器内的所有元素,而又无需暴露该容器的内部表达方式。
迭代器共分为五种,分别为: 输入迭代器(Input iterator)、输出迭代器(Output iterator)、前向迭代器(Forward iterator)、双向迭代器(Bidirectional iterator)、随机存取迭代器(Random access iterator)。
// 五种迭代器类型
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
// iterator 模板
template <class Category, class T, class Distance = ptrdiff_t,
class Pointer = T*, class Reference = T&>
struct iterator
{
typedef Category iterator_category;
typedef T value_type;
typedef Pointer pointer;
typedef Reference reference;
typedef Distance difference_type;
iterator模板共包含5个参数,用来定制不同类型的迭代器。
Category:这是一个用于指定迭代器类别(iterator category)的模板参数。迭代器类别用于描述迭代器的特性和行为,例如,输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器等。不同类别的迭代器具有不同的操作和能力。
T:这是迭代器所指向元素的类型。也就是说,iterator用于表示访问类型为T的元素的迭代器。
Distance:这是一个整数类型,表示两个迭代器之间可能存在的最大距离(通常为正整数),也就是容器的最大容量。默认情况下,它被设置为ptrdiff_t。
Pointer:这是一个指针类型,表示迭代器的指针类型。默认情况下,它被设置为T。
Reference:这是一个引用类型,表示迭代器的引用类型。默认情况下,它被设置为T&。
##############################################################后续再写
- util.h
位于include.h文件夹下,这个文件包含一些通用工具,包括 move, forward, swap 等函数,以及 pair 等 。
未完待续......