目前就容器而言,出现频率最高的当是vector。闲来无事,总结一二,请诸君指点一二...
原汁原味 llvm project
一,vector 构造
vector是一个动态的模板数组,元素类型灵活。
1.1 vector 创建
template <class _Tp, class _Allocator /* = allocator<_Tp> */>
class _LIBCPP_TEMPLATE_VIS vector
{
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI vector(size_type __n, const value_type& __x);
};
这是vector其中一个构造函数的申明。平常也经常使用到,比如:
vector<int> coll(10,1); //10个元素的vector,每个元素都是1
_LIBCPP_CONSTEXPR_SINCE_CXX20 :c++20及其之后的标准使能 constexpr 修饰
_LIBCPP_HIDE_FROM_ABI : 接口隐藏
这些宏不是特别影响分析,后面跳过。
1.2 vector 构造
template <class _Tp, class _Allocator>
_LIBCPP_CONSTEXPR_SINCE_CXX20
vector<_Tp, _Allocator>::vector(size_type __n, const value_type& __x)
{
auto __guard = std::__make_exception_guard(__destroy_vector(*this));
if (__n > 0)
{
__vallocate(__n);
__construct_at_end(__n, __x);
}
__guard.__complete();
}
这里介绍下:
1.2.1 异常守卫
auto __guard = std::__make_exception_guard(__destroy_vector(*this));
这里是创建一个异常监控,如果在构造过程中,出现异常,会调用__destroy_vector销毁vector中的元素,并且会产生一个assert 断言错误。一般,进程会出现Native Exeception, 产生tombstone。程序正常运行,在结束的时候,在complete里会关闭断言的产生,vector的对象也就生成了。
1.2.2 空间分配
// Allocate space for __n objects
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI void __vallocate(size_type __n) {
if (__n > max_size())
__throw_length_error();
auto __allocation = std::__allocate_at_least(__alloc(), __n);
__begin_ = __allocation.ptr;
__end_ = __allocation.ptr;
__end_cap() = __begin_ + __allocation.count;
__annotate_new(0);
}
大意是分配了n 个object的空间。
1.2.3 构造vector 元素
// Default constructs __n objects starting at __end_
// throws if construction throws
// Precondition: __n > 0
// Precondition: size() + __n <= capacity()
// Postcondition: size() == size() + __n
template <class _Tp, class _Allocator>
_LIBCPP_CONSTEXPR_SINCE_CXX20
void
vector<_Tp, _Allocator>::__construct_at_end(size_type __n)
{
_ConstructTransaction __tx(*this, __n);
const_pointer __new_end = __tx.__new_end_;
for (pointer __pos = __tx.__pos_; __pos != __new_end; __tx.__pos_ = ++__pos) {
__alloc_traits::construct(this->__alloc(), std::__to_address(__pos));
}
}
尾部构造插入元素
其他的形式的构造,大体上也是如此,大同小异。
要点:
1:异常监控机制
2:先分配空间,后才循环尾部插入元素
如果有更多的心得欢迎评论区留言。
二,push_back和emplace_back
2.1 push_back
template <class _Tp, class _Allocator>
_LIBCPP_CONSTEXPR_SINCE_CXX20
inline _LIBCPP_HIDE_FROM_ABI
void
vector<_Tp, _Allocator>::push_back(const_reference __x)
{
if (this->__end_ != this->__end_cap())
{
__construct_one_at_end(__x);
}
else
__push_back_slow_path(__x);
}
template <class _Tp, class _Allocator>
_LIBCPP_CONSTEXPR_SINCE_CXX20
inline _LIBCPP_HIDE_FROM_ABI
void
vector<_Tp, _Allocator>::push_back(value_type&& __x)
{
if (this->__end_ < this->__end_cap())
{
__construct_one_at_end(std::move(__x));
}
else
__push_back_slow_path(std::move(__x));
}
push_back的重载函数,实现策略一致。目的是调用了:
__construct_one_at_end(__x);
template <class ..._Args>
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI
void __construct_one_at_end(_Args&& ...__args) {
_ConstructTransaction __tx(*this, 1);
__alloc_traits::construct(this->__alloc(), std::__to_address(__tx.__pos_),
std::forward<_Args>(__args)...);
++__tx.__pos_;
}
函数的目的调用c++其它库函数,construct,构造对象,并插入vector尾部。
__tx是内部类,pos指向的是vector end位置。
2.2 emplace_back
#if _LIBCPP_STD_VER >= 17
typename vector<_Tp, _Allocator>::reference
#else
void
#endif
vector<_Tp, _Allocator>::emplace_back(_Args&&... __args)
{
if (this->__end_ < this->__end_cap())
{
__construct_one_at_end(std::forward<_Args>(__args)...);
}
else
__emplace_back_slow_path(std::forward<_Args>(__args)...);
#if _LIBCPP_STD_VER >= 17
return this->back();
#endif
}
看起来两个接口基本一致。emplace_back, 使用了完美转发,push_back 使用了move。重要的区别参数不一样:push_back 是value_type 类型。
这里是有几个彩蛋值得去试一试,比如:
1:vector 成员的构造函数为什么要是public的。
2:解释了为什么emplace_back 只调用了一次的构造函数。
完美转发了参数,转到成员的构造函数里了。
push_back 之真假美猴王示例:
#include <iostream>
#include <vector>
using namespace std;
struct Test{
int m;
Test(int n = 0):m(n){cout<<this<<",default construct\n";}
Test(const Test &obj):m(obj.m){cout<<this<<",copy construct\n";}
Test(Test &&obj):m(obj.m){cout<<this<<",move construct\n";}
};
void func() {
std::vector<Test> coll;
std::vector<Test> coll2;
Test obj1,obj2;
cout<<"\ntest push_back 1:\n";
coll.push_back(obj1);
cout<<"\ntest push_back 2:\n";
coll.push_back(move(obj2));
cout<<"\ntest emplace_back:\n";
coll2.emplace_back(1);
}
int main ()
{
func();
return 0;
}
结果:
0x7ffd9c3b1cfc,default construct
0x7ffd9c3b1cf8,default construct
test push_back 1:
0x1a482c0,copy construct
test push_back 2:
0x1a482e4,move construct
0x1a482e0,copy construct
test emplace_back:
0x1a482c0,default construct
示例,展示了push_back ,并不是如同想象中简单把对象push到vector里,真相是构造了一个新的对象放进了vector。这个对象是基于堆上的如同上例。再次使用vector里的对象,已经不是你原来的那个对象了。真假美猴王的彩蛋,get到了吗。
三,其他
size_type capacity() const _NOEXCEPT
{return static_cast<size_type>(__end_cap() - this->__begin_);}
_LIBCPP_NODISCARD_AFTER_CXX17 _LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI
bool empty() const _NOEXCEPT
{return this->__begin_ == this->__end_;}
看起来都是比较简单。通俗易懂。
总结:整个vector stl库里的源码比较易懂。如果进一步深究的应当是allocator的相关特性。
自定义allocator情况极少,这里也不再去追溯。
另外的vector创建的对象是基于堆创建,非栈。
库的实现,需要考虑的情况真多,实现的也很完美,隐藏的知识点也很多。