Effective STL~1 容器(条款1~12)
第1条:慎重选择容器类型
C++提供容器
- 标准STL顺序容器:vector、string、deque、list;
- 标准STL关联容器:set、multiset、map和multimap;
- 非标准顺序容器slist和rope。
slist(单向链表), rope(可持久化平衡树)是SGI STL(STL的一个实现)的内容,属于STL的扩展,C++11已结有更好的替代品,slist可以用forward_list替代,rope是“重型”string。(条款50)
参见:https://www.bbsmax.com/A/QV5ZZ2bV5y/
- 非标准的关联容器:hash_set、hash_multiset、hash_map、hash_multimap;(条款25)
- vector
作为string的替代;(条款13) - vector作为标准关联容器的替代。(条款23)
- 几种标准的非STL容器:数组、bitset、valarray、stack、queue、priority_queue。(条款16,18)
vector,动态数组,默认应该使用的序列类型;
list,双向循环链表,当需要频繁插入和删除数据时,应使用的序列类型;
deque,双端队列,头部和尾部插入和删除数据。
2类STL容器
连续内存容器(contiguous-memory container):又称基于数组的容器(array-based container),把元素存放在一块或多块(动态分配的)内存中,每个块内存有多个元素,按地址顺序存储。这种类型容器有标准容器vector、string、deque,也有非标准的rope。
基于节点的容器(node-based container):在每个(动态分配的)内存块中只存放一个元素,元素的插入和删除只影响指向节点的指针,不影响节点内容。这类型容器有链表容器,如list,slist;还有标准的关联容器(通常是AVL树实现)。
如何选择容器?
主要从 1)插入、删除元素的方式和效率;2)查询元素方式和效率;3)是否支持引用计数;4)是否关心排序;5)插入、删除异常,是否需要回滚;6)是否一定需要标准STL(是否支持扩展STL)。等几个方面考察,以决定使用哪个容器。
[======]
第2条:不要试图编写独立于容器类型的代码
当编写自己的容器、迭代器、算法时,不要把容器的概念泛化
比如为vector保留deque或list特有的功能代码,这部分代码称为独立于容器的代码(container-independent code)。为一种容器编写兼容另一种容器的泛化代码,通常是无意义的。比如,只有顺序容器才支持push_back/push_front,只有关联容器才支持count和lower_bound(map::lower_bound是返回第一个指向key >= 参数的元素),为其他容器编写这些是没有意义的。
如果要编写支持大多数通用的顺序容器的代码,那么就只能使用它们的交集,意味着放弃reserve、capacity等特定容器的操作。
建议的方式:为不同容器编写不同代码。
为不同容器类型使用封装技术,定义新类型名称
对容器类型和迭代器类型使用类型定义(typedef for 具体类型,using for template)
// 不要使用这种又长又臭的绑定具体类型的代码
class Widget{ ... };
vector<Wdiget> vw;
Widget bestWidget;
... // set bestWidget
vector<Widget>::iterator i = find(vw.begin(), vw.end(), bestWidget);
而要这样写:
// 为vector<Widget>和其迭代器定义类型别名
class Widget{ ... };
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw;
Widget bestWidget;
...// set bestWidget
WCIterator i = find(cw.begin(), cw.end(), bestWidget);
好处是:
1)当哪天发现vector
2)将类型名称长度简化,节省编写代码时间。
3)将选择的容器隐藏起来,封装到class中,客户无需关心这些细节。
[======]
第3条:确保容器中的对象拷贝正确而高效
STL容器存放(如insert或push_back)、取出(如front或back)的是对象的拷贝。排序算法如sort,全排列next_permutation或prev_permutation,移除一段元素remove,移除相邻重复元素unique或类似操作,旋转rotate或反转reverse,等等操作,对象将会被移动(拷贝)。
对象的拷贝利用的是copy constructor,或者copy assignment operator。如果class没有定义,当需要时,编译器会合成默认版本。
存在的问题:
1)大量容器对象的拷贝将造成内存和时间的浪费,拷贝对象可能会成为性能瓶颈。
2)在继承体系下,拷贝动作会导致剥离(slicing)。即将derived class对象放入base class对象的容器中,会导致derived class特有的那部分数据丢失,而且将无法使用virtual函数。
如何解决?
让容器存放指针而不是对象。指针的拷贝速度非常快,而且支持调用virtual函数。不过,指针容器也存在一些让人头疼的问题(见条款7和条款33)。用智能指针(smart pointer)来替代普通指针,见条款7。
STL容器设计目标
STL容器的设计思想是为了避免不必要的拷贝,而非疯狂拷贝。
比如,我们使用数组来存放若干Widget对象,不得不多次调用元素的默认构造函数构造对象
Widget w[MAX_NUM]; // 创建MAX_NUM个Widget对象的数组,每个对象都用默认构造函数构造对象
如果换成STL容器vector,创建时并不会调用任何元素的构造函数
vector
[======]
第4条:调用empty而不是检查size()是否为0
因为empty对所有标准容器都是常数时间O(1),常被实现为内联函数(inline function)。而对于一些list实现,size需要遍历链表,耗费线性时间O(n)。
list<int> lst;
if (lst.size() == 0) { ... } // 不推荐使用
if (lst.empty()) { ... } // 推荐使用
[======]
第5条:区间成员函数优先于与之对应的单元素成员函数
假设我们要将vector v2的后半部内容,拷贝到vector v1,要怎么办?
1)最直接想到使用显示循环遍历v2,然后push_back到v1:
vector<Widget> v1, v2; // 假设v1,v2是Widget的vector
...
v1.clear();
for (auto it = v2.begin() + v2.size() / 2; it != v2.end(); ++it) {
v1.push_back(*it);
}
然而,条款43告诉我们要尽量避免写显式的循环,因为这样的代码会比assign多做很多工作。
2)或许,会想到更简洁的办法copy,避免显式循环
v1.clear();
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
// 上面copy 实际上等价于
v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());
3)还有效率更高的办法,使用member function assign
v1.clear();
v1.assign(v2.begin() + v2.size() / 2, v2.end());
为什么使用区间member函数,如assign,而不是其相应的单元素成员函数,如push_back,单元素insert?
1)使用区间member函数,通常可以少些代码;
2)使用区间member函数通常会得到意图清晰,和更加直接的代码;
单元素版本insert在3个方面影响了效率,如果使用区间版本的insert,则3种影响都不存在。
1)不必要的函数调用。把元素插入容器,会导致多次调用insert函数;而区间版的insert,只做了一次函数调用(内联可能避免)。
2)把已有元素频繁移动到插入点之后;区间版的insert只会移动一次(若干个元素)。
3)重复的单元素插入,可能导致如vector内存重新分配,然后拷贝元素到新内存中,造成大量时间浪费;区间版的insert知道最终会插入多少元素,不会频繁申请内存、拷贝数据。
为什么assign比copy更高效?
1)copy掩盖了insert数据的事实,而直接调用insert member函数清晰表达了这点;
2)copy模板被实例化后,基于copy的代码和使用显式循环的代码几乎相同。因为插入迭代器也是调用的push_back。
哪些member函数支持区间操作?
- 区间创建:用1个标准容器区间,构造另1个标准容器
// 所有标准容器都提供构造函数形式
container::container(InputIterator begin, InputIterator end);
// 顺序容器区间创建示例
list<int> v2 = { 1,2,3 };
list<int> v1(v2.begin(), v2.end());
- 区间插入:insert第1个参数指明在哪插入,第2、3个参数指明源区间的开始和结束位置
// 顺序容器提供insert区间形式
void container::insert(iterator position, InputIterator begin, InputIterator end);
// 关联容器提供insert区间形式,省去了position参数
void container::insert(InputIterator begin, InputIterator end);
// 顺序容器insert区间示例
list<int> v2 = { 1,2,3 };
v1.insert(v1.end(), v2.begin(), v2.end());
- 区间删除:erase对于顺序和关联容器,返回值不同
// 顺序容器形式
iterator container::erase(iterator begin, iterator end);
// 关联容器形式
void container::erase(iterator begin, iterator end);
注意:erase内存反复分配,对vector和string不适用,后者调用erase时内存大小不变,因为后者能自动扩展内存,没有必要反复释放、申请内存。
- 区间赋值:assign
// 所有标准容器的区间赋值形式
voidcontainer::assign(iterator begin, iterator end);
注意:assign只是赋值,并不会改变内存大小。使用assign的前提是调用对象有足够内存大小。
[======]
第6条:当心C++编译器最烦人的分析机制
一个常见的错误:
class Widget{ ... } // 假定Widget有default构造函数
Widget w(); // 这句是做了什么?
上面的语句并非调用default构造函数来定义Widget对象w,而是声明了一个名为w的函数,不带任何参数,并返回Widget对象。
这是因为一个C++普遍规律:将表达式尽可能解释为函数声明。
声明一个double参数并返回int的函数:
// 下面3个都是函数f的等价声明式
int f(double d); // 常用的函数声明
int f(double (d)); // d两边括号被忽略
int f(double); // 参数名被忽略
声明一个函数,接受的参数是一个指向不带任何参数的函数指针,参数指向的函数返回double:
// 下面3个都是函数g 的等价声明式
int g(double (*pf)()); // g以指向函数的指针为参数
int g(double pf()); // pf为隐式指针
int g(double ()); // 省去参数名
我们再看一个例子:
假设我有一个存放int整数的文件,想把这些整数都拷贝到一个list中。下面做法能实现码?
ifstream dataFile("ints.data");
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>()); // 本想定义对象data,然而此处声明了函数data
想法是将一对istream_iterator传入list的区间构造函数,从而把文件中的整数全部拷贝到list。
然而,并不会如愿,因为第二句声明了一个函数data,返回值是list
1)第一个参数是dataFile 类型是istream_iterator
2)第二个参数没有名称,是一个指向不带参数的函数指针,该函数返回一个istream_iterator
如何解决这个问题?让编译器将data识别为对象定义?
方法1)通过为“形式参数”加上括号,让其非法,从而绕过分析机制。
list<int> data((istream_iterator<int>(dataFile)), istream_iterator<int>()); // 形参用"()"括起来是非法的,从而避免被解释为函数声明
缺点:并非所有编译器都支持,部分编译器可能会报错。
方法2)在对data的定义中,避免使用匿名istream_iterator对象,而是给这些迭代器一个名词:
ifstream dataFile("ints.data");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);
可以有效避免产生二义性。
[======]
第7条:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉
STL容器自身析构时,会自动释放包含的对象。然而,当容器存储的是指针时,却不会自动析构指针所指对象,也不会调用delete。
{
Widget{...};
vector<Widget*> vec;
for (int i = 0; i < VEC_NUM; ++i)
vec.push_back(new Widget);
...
} // 会发生Widget的泄漏
为避免发生内存泄漏,可通过以下2种方式解决:
1)在容器释放前,主动删除指针所指对象
为避免显式循环,以用for_each为例,释放vector
// 形式1:继承自unary_function,将class变成一个函数对象。适用于C++11以前,C++11以后不推荐,C++17以后移除
template<typename T>
struct DeleteObject : public unary_function<const T*, void> {
void operator()(const T* ptr) const
{
delete ptr;
}
};
// 形式2:直接定义一个可调用对象,重载operator()。适用于C++11以后
template<typename T>
struct DeleteObject2{
void operator()(const T* ptr) const
{
delete ptr;
}
};
// 形式3:定义lambda表达式。适用于C++11以后
// 客户端使用3种形式释放容器指针所指对象
// 3种形式作用是等价的
void f()
{
...
for_each(vec.begin(), vec.end(), DeleteObject<int>());
for_each(vec.begin(), vec.end(), DeleteObject2<int>());
for_each(vec.begin(), vec.end(), [](int *p) { delete p; }); // lambda表达式
}
上面的做法有一个缺陷:通过class template指明了要删除对象的类型如string,没有virtual析构函数,而实际存储的是指向string派生类对象的指针,这样通过指针删除派生类对象会产生不确定行为。
// 错误示例:通过基类指针delete删除派生类对象,而基类又没有virtual析构函数
// 虽然不建议继承没有virtual析构函数的class,但有人却这么做了,而编译器也不会阻止
class SpecialString: public string { ... };
void f()
{
deque<SpecialString*> dssp;
...
for_each(dssp.begin(), dssp.end(), DeleteObject<string>()); // 会导致不确定行为!因为基类string没有virtual析构函数,通过基类的指针删除派生类对象
for_each(dssp.begin(), dssp.end(), DeleteObject<SpecialString>()); // OK
}
我们没办法避免有人设计继承自没有virtual析构函数的class,但可以确保容器实际存放的类型与DeleteObject函数对象中要delete的对象类型保持一致。那就是将class template修改为function template。
// 形式4:形式1和2的改进,以函数模板形式定义可调用函数对象,参数类型由实参决定
struct DeleteObject {
template<typename T> // 注意这里的变化:将类模板修改成了函数模板
void operator()(const T* ptr) const
{
delete ptr;
}
};
void f()
{
deque<SpecialString*> dssp;
for_each(dssp.begin(), dssp.end(), DeleteObject()); // OK: 实参是什么类型,函数模板参数T就是什么类型
}
2)使用智能指针,自动释放对象。
方式1)还存在异常安全的问题,比如SpecialString创建后、for_each调用前就抛出异常,则会发生资源泄漏。而使用引用计数功能的智能指针,可以避免这个问题,也无需手动释放内存。
void f()
{
typedef shared_ptr<Widget> SPW;
vector<SWP> vwp;
for (int i = 0; i < VEC_NUM; ++i)
vwp.push_back(SPW(new Widget)); // 容器中存放 shared_ptr<Widget>,容器销毁时,由于引用计数归0,所绑定对象也会自动释放
...
}
[======]
第8条:切勿创建包含auto_ptr的容器对象
auto_ptr 自C++11后,已被丢弃。建议使用unique_ptr替代。
原因:
1)auto_ptr是不可移植的。
2)auto_ptr会导致奇怪的行为。
拷贝一个auto_ptr时,所指对象所有权被移交给拷入的auto_ptr上,自身被置为NULL。=> 拷贝一个auto_ptr意味着改变它的值。
例,
// auto_ptr之间的拷贝会导致奇怪的行为
auto_ptr
auto_ptr
pw1 = pw2; // pw1指向pw2的Widget;pw2被置为NULL
[======]
第9条:慎重选择删除元素的方法
删除容器中元素取决于具体的容器类型,推荐的删除元素方法概括:
- 要删除容器中特定值的所有对象
如果容器是vector、string或的确,则使用erase-remove;
如果容器是list,则使用list::remove;
如果容器是一个标准关联容器,则使用member函数erase;
- 要删除容器中满足特定判别式(条件)的所有对象
如果容器是vector、string或deque,则使用erase-remove_if;
如果容器是list,则使用list::remove_if;
如果容器一个标准关联容器,则使用remove_copy_if和swap,或者写一个循环来遍历容器中的元素,当把迭代器传给erase时,要对它进行后缀递增;
- 要在循环内部做某些(除删除对象外的)操作
如果容器是一个标准顺序容器,则写一个循环来遍历容器中的元素,记住每次用erase时,要用它的返回值更新迭代器;
如果容器是一个标准关联容器,则写一个循环来遍历容器中的元素,记住当把迭代器传给erase时,要对迭代器做后缀递增;
删除特定值的所有对象
例如,对于连续内存的顺序容器(vector、deque或string),最好的办法是用erase-remove(条款32):
Container<int> c;
c.erase(remove(c.begin(), c.end(), 100)); // 当c是vector、string、deque时,erase-remove习惯用法是删除特定值元素的最好办法
对于(非连续内存)list、forward_list,上面方法也适用,不过使用list::remove更有效(条款44)。
c.remove(100);
对于关联容器,应该使用erase。因为关联容器没有member函数remove(条款22),使用remove算法可能会覆盖容器的值(条款32),同时破坏容器。
c.erase(100);
关联容器member函数erase的2个优点:1)效率高,需要O(logn)时间开销。2)基于等价而不是相等(见条款19)。
删除满足特定判别式(predicate)的对象
见条款39,。使用下面的判别式(predicate)返回true的每个对象:
bool badValue(int x); // 返回x是否为“坏值”
对于顺序容器(vector、string、deque、list、forward_list),用remove_if做特定判别式
// 当c是vector、string、deque时,remove_if + erase是删除使badValue返回true的对象的最好办法
c.erase(remove_if(c.begin(), c.end(), badValue(), c.end());
// 当c是list时,remove_if是删除使badValue返回true的对象的最好办法
c.remove_if(badValue);
对于关联容器(map、set),有两种办法:
1)用remove_copy_if 将需要的值拷贝到新容器,然后把原来容器和新容器互换(swap)。
优点是易于编码,缺点是效率低,因为要拷贝所有不被删除的元素。
AssocContainer<int> c;
AssocContainer<int> goodValues;
...
// 保存不被删除的元素到新容器goodValues
remove_copy_if(c.begin(), c.end(), insert(goodValues, goodValues.end()), badValue);
// 交换c和goodValues
c.swap(goodValues);
2)直接擦除要删除的元素
代码简单,但容易出错
AssocContainer<int> c;
...
for (auto i = c.begin(); i != c.end(); ) {
if (badValue(*i))
i = c.erase(i); // 等价于 c.erase(i++);
else ++i;
}
因为成员函数erase删除元素时,原来的迭代器i就被破坏了,并且erase返回的迭代器代表的就是删除元素的下个位置。因此对要删除元素的迭代器i++,或者让i = c.erase(i) 都是可行的。
每次删除对象时,都写log记录
对于连续内存的顺序容器(vector、string、deque),不能再用erase-remove惯用方法,因为无法用erase或remove向日志中写信息。对这类容器也不能单独调用erase,不仅会使被删除元素的迭代器无效,也会使被删除元素之后的所有迭代器都无效。用i++、++i形式,也是一样无效。可以利用erase的返回值,指向被删除元素的下一个元素的迭代器。
for (auto i = c.begin(); i != c.end(); ) {
if (badValue(*i)) {
logFile << "Erasing " << *i << '\n';
i = c.erase(i); // 把erase返回值赋值给i,使i值保持有效
}
else ++i;
}
这种方法对非连续内存的顺序容器、关联容器也都适用。
关于std::remove,std::remove_if,详见:https://www.cnblogs.com/fortunely/p/15694743.html
[======]
第10条:了解分配子(allocator)的约定和限制
STL内存分配子allocator负责分配和释放原始内存(raw memory)。
分配子(allocator
class Widget { ...};
allocator<Widget>::pointer p; // p的类型为Widget*
allocator<Widget>::reference r = &p; // r的类型为Widget&
STL的实现可以假定,所有属于同一种类型的分配子对象都是等价的,并且相互比较的结果总是相等的。
什么意思?请看下面的代码:
list<Widget, SAW> l1;
list<Widget, SAW> l2;
// ...
l1.splice(l1.begin(), l2); // 将l2的节点移动到l1前面
但是,假定同一类型的分配子是等价的,就会有非常苛刻的限制:意味着可移植的分配子对象,在不同的STL下都能正确工作,是不能有状态(state)的。简单来说,就是可移植的分配子不可以有任何非静态的数据成员,不能有会影响其行为的数据成员。这是程序员应该遵守的规则,而非编译器要求。
C++标准指出:
鼓励实现者提供......支持不相等实例的库。在这样的实现中,......当对分配子实例的比较不相等时,容器和算法的语义取决于该实现。
----这有利于STL实现者,但对STL使用者毫无帮助。
这段说明适用条件:
1)你知道你正在使用的STL实现支持不等价的分配子;
2)你愿意钻到它们的文档中,以决定该实现锁定义的“不相等”的分配子行为对你来说是否可以接受;
3)你不考虑把你的代码移植到那些利用了C++标准明确给予的扩展能力的实现;
allocator<T>::allocate与operator new
相同点在于:分配原始内存。
不同点在于:
1)operator new指明具体要分配多少byte内存,而allocator
函数原型见:
void* operator new(size_t bytes);
pointer allocator<T>::allocate(size_type numObjects);
2)返回值不同,operator new返回void,allocator
为什么很少使用T的分配子?
考虑自定义llist容器,用来表示一个链表,链表节点ListNode。当实例化模板llist
template <typename T, typename Allocator = std::allocator<T>>
class llist {
private:
struct ListNode // 链表节点
{
T data;
ListNode *prev;
ListNode *next;
};
Allocator alloc; // 类型为T的对象的分配子
};
使用rebind嵌套分配子,就可以得到ListNode的分配子
// rebind嵌套分配子声明
template <typename T>
class myallocator {
public:
template<typename U>
struct rebind
{
using other = myallocator<U>;
};
};
template <typename T, typename Allocator = myallocator<T>>
class llist {
private:
struct ListNode
{
T data;
ListNode *prev;
ListNode *next;
};
using _allocator_type = typename Allocator::template rebind<ListNode>::other; // ListNode分配子类型
_allocator_type alloc; // ListNode分配子对象
};
小结
- 你的分配子是一个模板,模板参数T代表你为它分配内存的对象类型;
- 提供类型定义pointer和reference,但始终让pointer为T*,reference为T&;
- 别让你的分配子拥有随对象而不同的状态(per-object state),即分配子通常不应该有非static数据成员,除非你明确可以接受它;
- 传给allocator
::allocate成员函数的参数是内存T对象的个数,而非byte数。同时,函数返回T*指针,即使尚未调用T构造函数; - 一定要提供嵌套的rebind模板,因为标准容器依赖该模板;
[======]
第11条:理解自定义分配子的合理用法
如果STL默认内存管理器(std::allocator
- 假定你想使用malloc和free内存模型来管理一个位于共享内存的堆
// 自定义内存分配、回收算法
void* mallocShared(size_t bytesNeeded);
void* freeShared(void* ptr);
// 共享内存分配子模板
template<typename T>
class SharedMemoryAllocator
{
public:
using pointer = T*;
using size_type = size_t;
pointer allocate(size_type numberObjects, const void* localityHint = 0)
{
return static_cast<pointer>(mallocShared(numberObjects * sizeof(T)));
}
void deallocate(pointer ptrToMemory, size_type numberObjects)
{
freeShared(ptrToMemory);
}
...
};
这样使用SharedMemoryAllocator:
typedef vector<double, SharedMemoryAllocator<double>> SharedDoubleVec;
...
// 某个代码块
{
...
SharedDoubleVec v; // 创建一个vector,其元素位于共享内存中,而v自身位于stack中
...
}
v使用SharedMemoryAllocator分配内存,而SharedMemoryAllocator的分配内存方法allocate是使用mallocShared在共享内存上分配的。而v自己是位于函数栈的对象。为了让v的内容和v自身都放到共享内存中,可以这样:
{
void* pVectorMemory = mallocShared(sizeof(SharedDoubleVec)); // 为SharedDoubleVec对象分配足够内存
SharedDoubleVec* pv = new (pVectorMemory) SharedDoubleVec; // 使用placement new在指定内存pVectorMemory中创建一个SharedDoubleVec
// ...
pv->~SharedDoubleVec(); // 析构共享内存中的对象(vector中的内容)
freeShared(pVectorMemory); // 释放最初分配的那一块共享内存(vector自身)
}
工作流程:
1)获取一块共享内存;
2)然后在其中构造一个将共享内存作为自己内部内存使用的vector;
3)当用完了该vector后,调用它的析构函数,然后释放它锁占用的内存;
除非确实需要一个位于共享内存中的容器(而不仅仅是把它的元素放到共享内存中),否则,建议避免这种手工的“分配、构造、析构、释放”(allocate、construct、destroy、deallocate)四部曲。
- 假设你有2个堆:class Heap1和class Heap2,每个堆都有相应的静态成员函数执行内存分配和释放操作
class Heap1
{
public:
static void* alloc(size_t numBytes, const void* memoryBlockToBeNear);
static void dealloc(void* ptr);
};
class Heap2
{
public: // 同Heap1的alloc/dealloc接口
static void* alloc(size_t numBytes, const void* memoryBlockToBeNear);
static void dealloc(void* ptr);
};
如果你想把一些STL容器的内容放在不同的堆中,可以编写一个分配子SpecialHeapAllocator,可以使用像Heap1/2这样的class来完成实际的内存管理:
template<typename T, typename Heap>
class SpecialHeapAllocator
{
public:
typedef T* pointer;
typedef size_t size_type;
pointer allocate(size_type numObjects, const void* localtyHint = 0)
{
return static_cast<pointer>(Heap::alloc(numObjects * sizeof(T)), localtyHint);
}
void deallocate(pointer ptrToMemory, size_type numObjects)
{
Heap::dealloc(ptrToMemory);
}
...
};
最后,使用SpecialHeapAllocator把容器的元素聚集到一起:
class Widget {...};
// 在v和s的元素都在Heap1中
vector<int, SpecialHeapAllocator<int, Heap1>> v;
set<int, SpecialHeapAllocator<int, Heap1>> s;
// 在L和M的元素都在Heap2中
list<Widget, SpecialHeapAllocator<Widget, Heap2>> L;
map<int, string, less<int>,
SpecialHeapAllocator<pair<const int, string>,
Heap2>> m;
[======]
第12条:切勿对STL容器的线程安全性有不切实际的依赖
关于线程安全,对于一个STL实现你最多只能期望2点:
- 多个线程读是安全的。
多个线程可以同时读一个容器的内容,并且保证是正确的。自然地,在读的过程中,不能对容器有任何写入操作。
- 多个线程对不同的容器写入操作是安全的。
多个线程可以同时对不同的容器做写入操作。
然而,STL实现并不保证上面2点,因为做到线程安全,将会很困难。
考虑当一个库试图实现完全的容器线程安全性时,可能采取的方式:
- 对容器成员函数的每次调用,都锁住容器直到调用结束;
- 在容器所返回的每个迭代器的生存期结束前,都锁住容器(如通过begin或end调用);
- 对于作用于容器的每个算法,都锁住该容器,直到算法结束。
这样做起始没有意义,因为算法无法知道所操作的容器(条款32)。这里仅讨论。
这里介绍两种方法,手工实现STL在多线程中的同步控制。
- 自定义Lock类,用RAII管理互斥锁
#include <mutex>
static mutex mtx;
void getMutexFor(vector<int> &vec)
{
mtx.lock();
}
void releaseMutexFor(vector<int> &vec)
{
mtx.unlock();
}
// RAII管理互斥锁资源
template<typename Container>
class Lock {
public:
Lock(const Container& container) : c(container)
{
getMutexFor(c); // 构造函数中获得互斥锁
}
~Lock()
{
releaseMutexFor(c); // 析构函数中释放互斥锁
}
private:
const Container& c;
};
// 客户端
{
vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
Lock<vector<int>> lck(v);
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end())
{
*first5 = 0;
}
}
- 使用STL库lock_guard
#include <mutex>
static mutex mtx;
// 客户端
{
vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
lock_guard<mutex> lock(mtx);
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end())
{
*first5 = 0;
}
}