STL源码个人学习与分析记录 ——Construct()与destroy()
作为一位C++程序员,直接使用STL标准中的各类容器(vector,list,map,hash)以及各种泛型算法完成一些任务是十分正常而又便捷的。但是,只会使用工具而不知道工具的原理和构造方法,那肯定是不可能完全搞懂和掌握手中这些强力的工具的。
为了搞懂这些STL容器的底层逻辑和代码结构,想找找有没有相关的介绍书籍,找来找去只有这本《STL源码分析》,一看作者,侯捷老师,好嘛,这不是《Effective C++》的中文译者,看来又可以享受和领略侯老师独特的构词造句了,哈哈哈。
1. 目前所使用的编译器
1.1 编译器:MinGW Version:13.2.0
MinGW(Minimalist GNU for Windows) 是一个用于 Windows 平台的开发工具集,它提供了一组 GNU 工具和库,可以用于编译和构建本地的 Windows 应用程序。MinGW 的目标是在 Windows 环境下提供类似于 Unix/Linux 环境下的开发工具,使开发者能够轻松地在 Windows 上编写和编译 C、C++ 等程序。
1.2 MinGW的主要组件
-
1) GCC(GNU Compiler Collection): GCC 是一个开源的编译器套件,支持多种编程语言,包括 C、C++、Fortran 等。在 MinGW 中,GCC 被用来编译和生成 Windows 平台下的可执行文件。
-
2) Binutils:Binutils 是一组用于处理二进制文件的工具,包括汇编器、链接器、目标文件处理器等。在 MinGW 中,Binutils 用于将编译后的源代码转换为可执行文件。
-
3 )运行时库(Runtime Libraries): MinGW 提供了 Windows 下所需的 C 和 C++ 运行时库,这些库是在编译和链接时所需要的,以便在 Windows 环境下运行程序。
-
4)MSYS(Minimal SYStem): MSYS 是一个轻量级的 Unix-like 环境,它在 Windows 上提供了一些基本的 Unix 命令行工具,使开发者能够更方便地使用命令行进行开发和构建。
MinGW 可以与其他开发工具集(如 Visual Studio)一起使用,但它的重点是提供一个简单的方式来在 Windows 上进行开发,无需依赖复杂的集成开发环境(IDE)。MinGW 的使用可以让开发者更接近标准的开发环境,同时也方便了跨平台的开发。
1.3 写文初衷
在阅读这本书之前其实存在一定的疑虑,最主要的原因为该书的发行时期,2002年,距我目前已有22年之隔,某呼上对该书的评论中也主要指出该书中的代码已经过时,不应当再阅读此书。但思索再三,目前该本教材的内容正是我所需求的,而书中的那句“源码之下,毫无秘密”也提醒了我,既然书中代码已经过时,那为何不直接阅读已经安装的编译器GCC中的STL源代码文件呢?因此,顺手写一篇博客记录一下阅读源码的过程。
2.构造与析构工具:Construct()与Destory()函数的定义
书中提醒到,SGI STL版本的Construct()与Destory()函数定义在<stl_construct.h>文件中,搜寻了MinGW的安装空间,也找到同名的文件(相对路径为 “…\MinGW\include\c++\13.2.0\bits”,其中“13.2.0”为我所使用的编译器版本),以下是对应于书中源码的部分代码截取。
2.1 Construct()函数的定义
#if __cplusplus >= 201103L //如果当前使用的C++标准为C++11以及更高标准的话,则可以定义一个可变参数的函数模版
template<typename _Tp, typename... _Args>
_GLIBCXX20_CONSTEXPR
inline void
_Construct(_Tp* __p, _Args&&... __args) //传入一个指针__p指向已经分配的内存空间,以及一个可变参数_Args
{
#if __cplusplus >= 202002L
if (std::__is_constant_evaluated())
{
// Allow std::_Construct to be used in constant expressions.
std::construct_at(__p, std::forward<_Args>(__args)...); //如果C++标准为C++20及其以上且当前函数调用的上下文环境为常量时,调用std::construct_at()完成构建工作
return;
}
#endif
::new((void*)__p) _Tp(std::forward<_Args>(__args)...);
//否则就调用全局定位new函数,在指针__p所指内存空间调用类型_Tp的带有初始化器的构造函数实现对象的构造。
}
#else //如果所使用的C++标准低于C++11,则定义下述的Construct函数,因为可变参数是C++11引入的新特性!!
template<typename _T1, typename _T2>
inline void
_Construct(_T1* __p, const _T2& __value)
{
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 402. wrong new expression in [some_]allocator::construct
::new(static_cast<void*>(__p)) _T1(__value);
}
#endif
2.1.1 “__cplusplus”的含义
__cplusplus是一个预定义宏,用来指明当前编译器在使用的C++标准版本,它由编译器自动定义。以下是常见的“__cplusplus”的值
-
C++98/03:
199711L
该数值对应于原始的 C++98 standard (ISO/IEC 14882:1998) 和它的 2003 revision (C++03). -
C++11:
201103L
该值对应于 C++11 standard (ISO/IEC 14882:2011). -
C++14:
201402L
该值对应于 C++14 standard (ISO/IEC 14882:2014). -
C++17:
201703L
该值对应于 C++17 standard (ISO/IEC 14882:2017). -
C++20:
202002L
该值对应于 C++20 standard (ISO/IEC 14882:2020). -
C++23:
202302L
该值对应于 C++23 standard ( ISO/IEC 14882:2023).
2.1.2 编译组态
由于不同的编译器厂商对C++语言特性的支持程度不同,为了使得程序库能够拥有较强的移植能力,不同版本的STL标准库都会准备一个环境组态文件来定义一系列的常量,以表示该组态能否成立。书中给出了具体的案例说明和示例代码,此处就不再赘述。(SGI STL版本的环境组态文件为<stl_config.h>,GCC的组态文件为<c++config.h>,可以在…/bits目录下找到)
例如,像上述代码中的第二行出现了“_GLIBCXX20_CONSTEXPR”,它的含义如下:
-
1)_GLIBCXX:这个前缀通常在 libstdc++ 中使用,用于定义特定于 GNU C++ 标准库实现的宏和标识符。它有助于防止与用户代码发生命名冲突。
-
2)20:这个数字指的是 C++20 标准。宏 _GLIBCXX20_CONSTEXPR 专门用于 C++20 中可用的功能。
-
3)CONSTEXPR: 这是 C++ 11 中引入的 C++ 关键字,代表 “常量表达式”,用于定义可在编译时求值的函数或变量。
2.1.3 (void*)__p
代码中但凡涉及到调用全局new函数的时候,都会强制将指针的类型转换为(void*),目的是为了显式地指出当前该指针p所指向的内存空间是原始内存空间,与任何数值类型都无关。
2.2 Destroy()函数的定义
Destroy()函数的版本一(比较简单,不多解释)
/**
* Destroy the object pointed to by a pointer type.
*/
template<typename _Tp>
_GLIBCXX14_CONSTEXPR inline void
_Destroy(_Tp* __pointer)
{
#if __cplusplus > 201703L
std::destroy_at(__pointer);
#else
__pointer->~_Tp();
#endif
}
Destroy()函数的版本二: 接受两个迭代器,并析构[__first,__last)范围内的对象
//SGI STL与GNU STL在_Destroy_aux的实现上存在了分歧,
//SGI STL采用的是对函数模版的重载,
//而GNU则是通过定义类模版和对模版进行特化的方式实现了两种版本的Destroy_aux,并且正在发挥作用的是嵌套在Destroy_aux模版中的__destroy的成员函数模板。
template<bool>
struct _Destroy_aux
{
template<typename _ForwardIterator>
static _GLIBCXX20_CONSTEXPR void
__destroy(_ForwardIterator __first, _ForwardIterator __last)
{
for (; __first != __last; ++__first)
std::_Destroy(std::__addressof(*__first)); //如果迭代器所指对象拥有非trivial析构函数,则调用版本一的Destroy()函数。
//
}
};
template<>
struct _Destroy_aux<true>
{
template<typename _ForwardIterator>
static void
__destroy(_ForwardIterator, _ForwardIterator) { }
};
/**
* Destroy a range of objects. If the value_type of the object has
* a trivial destructor, the compiler should optimize all of this
* away, otherwise the objects' destructors must be invoked.
*/
template<typename _ForwardIterator>
_GLIBCXX20_CONSTEXPR inline void
_Destroy(_ForwardIterator __first, _ForwardIterator __last)
{
typedef typename iterator_traits<_ForwardIterator>::value_type
_Value_type; //利用迭代器的trait模版,编译期间获得当前迭代器所指对象的类型
#if __cplusplus >= 201103L
// A deleted destructor is trivial, this ensures we reject such types: 确保迭代器所指对象的析构函数没有被delete
static_assert(is_destructible<_Value_type>::value,
"value type is destructible");
#endif
#if __cplusplus >= 202002L
if (std::__is_constant_evaluated())
return std::_Destroy_aux<false>::__destroy(__first, __last);
#endif
//根据当前迭代器所指对象类型是否具有trivial析构函数来抉择是调用普通版本的_Destroy_aux类的成员函数__destroy,还是调用特化版本的
//_Destroy_aux<true>类的__destroy()成员函数。
std::_Destroy_aux<__has_trivial_destructor(_Value_type)>::
__destroy(__first, __last);
}
3. 总结
从上述代码的分析中可以看出,两个版本的STL的Construct()与Destory()基本工具的定义在底层实现上大差不差,Construct()工具主要是在已分配的空间中利用定位New运算符,结合所给初值调用数值类型的构造函数完成对象的构造。由于C++11引入了可变参数模版,所以Construct()的函数参数做了一些调整,原本的左值引用变为了模版参数包(Template Parameter Pack)。Destory()工具则是根据迭代器所指对象是否拥有trivial析构函数来决定是否通过调用对象的析构函数完成销毁任务,或是什么也不做。