item1 慎重选择容器类型
1 STL容器分类
标准
序列
vector string deque list
关联
set multiset map multimap
非标准
序列
slist rope( “重型”string)
关联
hash
标准
非STL容器
数组 bitset valarray
stack queue priority_queue
另一种分类方法: 连续内存/基于节点 的容器
(1) 连续内存容器
元素存放在>=1块(动态分配的)内存中,
每块内存中存多个元素
插/删时, 同一内存块中 其他元素要 向前或向后移动
以便 让出空间/填充空隙
-> 移动影响到效率(item5/14条)和异常安全性
连续内存容器
标准
vector string deque
非标准
rope
(2) 基于节点 的容器
每个内存块 只放1个元素
插/删 只影响指向节点的指针, 而不影响节点本身的内容, 元素的值无需移动
链表 list/slist
标准
关联容器
非标准
哈希容器
2 容器替代
(1) vector
(2) vector 时空都优于 标准关联容器 的case: item23
(3) "数组优于STL容器" 的 case: item16
(4) bitset 优于 vector
数组也可以被用于STL算法
, 因为指针可用作数组的迭代器
3 容器选择
3.1 算法复杂性
是主要考虑因素时
大量插/删 在序列中间 -> list
大量插/删 在序列头或尾 -> deque
3.2 其他考虑
, 下面, 不考虑非STL容器
(1) 想在任意位置插
-> 选序列容器, 关联容器不行
(2) 要求元素排序
-> 避免哈希容器
(3) 要求的迭代器类型
随机
string vector deque
rope
双向
避免 slist
避免 哈希容器的一个常见实现(25条)
(4) 插/删时 避免移动元素
-> 避免连续内存的容器
(5) 数据布局需要和C兼容
-> 只能用 vector
(6) 查找速度
是主要考虑因素, 优先顺序: 哈希容器 -> 排序的 vector -> 标准关联
(7) 内部不能用 引用计数 -> 避免 string (大多实现用 RC) 和 rope -> 还想用字符串: 考虑 vector
(8) 对 插/删
, 需要 事务(失败时回滚)
语义 -> 基于节点
的容器
对 `区间多元素插入` 需要事务语义 -> list
异常安全代码需要事务语义
事务语义 用 连续内存实现 -> 性能代价, 代码也不那么直截了当: Exceptional C++ 17条
(9) 需要使 迭代器/指针/引用变为无效
的次数最少
基于节点的容器
插/删不会使...无效, 除非指向正在删除的元素
连续内存容器
插/删 一般会使指向该容器的迭代器 指针 引用 (及其之后的迭代器...)变为无效
(10) 在容器上用 swap, 不想使迭代器 指针 引用变为无效 -> 避免用 string, 因为 string 是 STL中在 swap过程中会导致 迭代器/指针/引用变为无效
的唯一容器
(11) deque 是迭代器可能会变为无效而指针和引用不会变为无效
的 唯一
STL标准容器
条件: 没删除, 插入只发生在容器尾
这些问题并没有涵盖所有的情形。如不同容器类型所采取的`不同的 内存分配策略`(10条和14条)
STL之外还有更多的选择
item2: 不要试图编写 独立于容器类型的代码
1 STL以泛化
为基础, 但是, 试图编写独立于容器的代码的泛化
几乎总是误入歧途或毫无意义
特化 泛化
数组 -> 所含对象 作参数 -> 容器
函数 -> 所用迭代器 作参数 -> 算法
指针 -> 所指对象 作参数 -> 迭代器
(1) 容器类型不同, 所提供的 迭代器类型不同
`标准 连续内存` 容器 `随机`访问迭代器
`标准 基于节点`的容器 `双向`迭代器
(2) 容器类型不同, 所支持的 很多成员函数不同
序列容器才有
push_front push_back
关联容器才有
count
对数时间的 lower_bound/upper_bound/equal_range 成员函数
(3) 容器类型不同, insert/erase 等基本操作
的 语义和原型不同
insert
对象被插入的位置
序列容器 位于被插入的位置处
关联容器 按其排序规则`将对象移动到适当位置上`
erase
返回值
序列容器 新的迭代器
关联容器 无
2 编写对3种 序列容器(vector/deque/list)通用的代码
, 只能使用它们功能的交集
不能用
push_front/pop_front : vector 不支持
operator[] : list 不支持
要求随机访问迭代器的操作 : list 不支持(只支持双向迭代器操作)
sort
stable_sort
partial_sort
nth_element
splice / 成员函数 sort : deque 和 vector 不支持, 仅 list 支持
reserve 或 capacity : deque 和 list 不支持, 仅 vector 支持
=> 你的 "泛化的序列容器" 将没有任何形式(非成员/成员)的 sort 可用
这些限制的根源: 不同类型
的序列容器, 使迭代器、指针和引用无效的规则不同
3 对关联容器, 编写
(1) 通用于 set 和 map
的代码, 几乎不可能
原因: set 存储单个对象, map存储 "一对" 对象
(2) 通用于 set 和 multiset( 或 map 和 multimap)
的代码, 很难
[1] `单个值的成员函数 insert` 对于 set/map 和与之对应的 multi 类型, `返回类型不同`
pair
insert (const value_type& val); // set
iterator
insert (const value_type& val); // multiset
[2] operator[] 只对 map 存在, 对 multimap 不存在
4 想 表面上隐藏容器相关信息
的常规方式: 封装
最简方法 对 容器类型
和其 迭代器类型
用 typedef
class W;
typedef vector WContainer;
WContainer wc;
W w;
// ...
WContainer::iterator iter = find(wc.begin(), wc,end(), w);
typedef 的封装只是词法上的, 无法隐藏 client 原本可见的容器相关信息
5 想 真正 隐藏容器相关信息 / 减少在替换容器类型时所需修改的代码
: 将容器隐藏到1个类
中, 并尽量 减少通过类接口(而使外部)可见的、与容器相关的信息
class CustomerList
{
private:
typedef list CustomerContainer;
typedef CustomerContainer::iterator CCIter;
CustomerContainer customerContainer;
public:
// ... // 减少通过类接口(而使外部)可见的、与容器相关的信息
};
后来, 需要快速确定前20% 的顾客 => 适合用 nth_element => vector/deque 实现
item3: 确保容器中的对象拷贝
正确而高效
1 拷贝对象
是STL的工作方式
(1) 通过 insert / push_back
类似操作存入 容器中的对象是 指定对象的拷贝
(2) 对象被存到容器中, 经常会进一步被拷贝
[1] vector/string/deque 插/删时, 现有元素通常会被移动(拷贝)
[2] 排序 / next_permutation / previous_permutation / remove / unique / rotate / reverse 类似操作, 对象被移动(拷贝)
2 copy 如何进行
copy constructor / copy assignment operator
内置类型(整型/指针): 按位拷贝
3 copy 的潜在问题
(1) 对象的拷贝 若很耗时
-> 性能瓶颈
(2) 对象的拷贝 有特殊含义
-> 对象放入容器引发错误(item8)
(3) 继承下的 copy
-> 剥离
slicing 问题:
[1] `派生类对象` 被插入/拷贝(通过基类 copy ctor)进 `基类对象容器`时, `派生部分丢失`
=> 向基类对象容器 插入派生类对象 几乎总是错误的
[2] 例2: item38
使 拷贝 高效, 正确, 防止剥离
的简单办法: 用 指针容器
而不是对象容器
拷贝指针(按位拷贝)的速度很快, 且总是会按你期望的方式进行
但, 指针容器有 令人头疼的、与 STL 相关的问题(item7/33)
可能更好
的选择: 智能指针
容器(item7)
4 STL容器
做了很多拷贝, 但它总的设计思想
是避免创建不必要的对象(需要时才会增长) => 避免不必要的拷贝
数组 vs. Vector
W wArr[N]; // 创建 N 个 Widget 对象
vector wVec; // 空 vector
wVec.reserve(N); // 需要时才会增长: 包含足够空间来容纳 N 个对象, 但并没有创建任何1个对象
与数组相比, STL容器更聪明: 你让它创建多少对象, 它就(通过拷贝)创建多少对象; 你让它创建时它才创建
STL容器 是在创建拷贝, 但与数组相比, 仍是迈出了一大步
item4: 调用 empty() 而不是检查 size()是否为0
if(c.size() == 0) 与 if(c.empty() )
本质上等价, 为什么偏向 empty: 对所有标准容器
都是 常数时间
1 有些 list::size() 为什么只能是线性时间
原因: list 独有的 成员函数 splice/链接操作
, 而 splice 与 size 只能其中之一为常数时间
负责 更新 所操作的链表(list)的大小
的 成员函数
, 不能是常数时间
, 只能是 线性时间
如何更新? 遍历 list 的整个数据结构, 边遍历边 数/count list 含多少个元素
(1) 若 size 不负责
更新..., 则 每个改变 list 大小的成员函数
都必须(负责) 更新...
size 可以是常数时间, 成员函数 splice 不能是常数时间
(2) 若 size 负责
更新..., 则 不要求其他成员函(包括 splice) 更新...
size 不能是常数时间, 成员函数 splice 可以是常数时间
2 例
list lst1;
list lst2;
// ...
list1.splice(lst1.end(),
lst2,
find(lst2.begin(), lst.end(), 5),
find(lst2.rbegin(), lst2.rend(), 10).base() ); /// base(): item28
链接后 list1 的元素数 = 链接前的元素数 + 链接过来的元素数( 必须 遍历该区间来数一数
才知道)
void splice (const_iterator position,
list& x, const_iterator first, const_iterator last);
void splice (const_iterator position,
list&& x, const_iterator first, const_iterator last);
只有 list 能把元素从一处链接到另一处而 不需要拷贝任何数据
, 许多选 list
的 client 是因为 list 提供了高效(常数时间)
的 链接(splice)操作
3 不同的 list 实现取决于实现者把 size 还是 splice 实现得最为高效
若你使用的 list
实现恰好是把 splice 常数时间操作放在第1位
, 则用 empty 而不是 size 更好, 因为 empty 总是常数时间
item5: 区间成员函数 优先
于对应的单元素成员函数
原因
[1] 代码量更少
[2] 意图更清晰
[3] 更高效: 3个方面
1 给定2个 vector, 使 v1的内容和v2的后半部分相同
的最简单操作
(1) 成员函数 assign(): 满分
v1.assign(v2.begin()+ v2.size()/2, v2.end() );
(2) 不止1个函数调用, 但 没有任何形式的循环: 优秀
v1.clear();
v1.insert(v1.end(), v2.begin()+ v2.size()/2, v2.end() );
(3) 1个 循环(效率低): // 及格
显式循环
vector v1, v2;
// ...
v1.clear();
for(vector::const_iterator ci = v2.begin() + v2.size()/2;
ci != v2.end();
++ci)
v1.push_back(*ci);
隐式循环: copy 中隐含循环
v1.clear();
copy(v2.begin() + v2.size()/2, v2.end(), back_inserter(v1) );
[1] 几乎所有 利用 插入迭代器(insert iterator)的方式(即利用 inserter/back_inserter/front_inserter)
来限定目标区间的 copy
,
都可以(也应该) 被替换
为 区间成员函数 insert
[2] 掩盖了 数据插入
的 copy -> 应替换为 更清晰表明了 数据插入的 insert
(4) 多个循环: 不及格
该测试的两个目的
(1) 提醒, 所有 标准序列容器(string / vector / list / deque)
都存在 成员函数 assign
: 极其方便、却为许多程序员所忽略
给容器一组全新的值时, 用 assign
而 operator= 不能满足要求
完全替换 / copy
同类型容器内容 赋值(assignment) operator=
(2) 揭示, 为什么 区间成员函数 优先
于 单元素成员函数: 避免用循环, 效率低
区间成员函数: 用`两个迭代器参数来 确定该成员操作所执行的 区间`
若不使用区间成员函数
, 就得用 显式的循环(item43
说明为什么要避免显式的循环)
2 效率角度
例: int 数组拷贝到 vector 前端
(1) 循环
1) 显式循环
调用 单元素 insert
vector::iterator insertLoc(v.begin() );
for(int i = 0; i < N; ++i)
{
insertLoc = v.insert(insertLoc, data[i]);
++insertLoc;
}
每次插入后必须
[1] 记录 insert 的返回值, 供下次进入循环时使用
[2] 更新 插入位置(insertLoc)
, 否则产生2个问题
1] insert 可能使 insertLoc 及其关联的迭代器失效
=> 第1次迭代后的所有循环 undefined behavior
2] 即使 insertLoc 有效, 不更新, 则每次插入都在 vector 最前面(v.begin() ) => 数组被 反序插入
到 v 中
2) copy 隐式循环
copy(data, data + N, inserter(v, v.begin() ) );
单元素 insert 与 copy+inserter 的效率分析相同
(2) 区间 insert
: 这3种影响不复存在
int data[N];
vector v;
//...
v.insert(v.begin(), data, data + N); // STL容器 和 C API 混合使用(item16)
循环 影响效率的3个方面
例: 向含 n 个元素的 vector
[1] 不必要的函数调用
: N 次函数调用
区间 insert: 节省 N-1 次函数调用
[2] 每次 insert 时, 原先每个元素后移1个位置
=> 总共: (n-1) * N 次 assignment + N 次 copy ctor
`(n-1) * N 次调用 W 的 赋值操作符(operator= ) + N 次调用 W 的 copy ctor`
最后1个元素被移动时调 copy ctor
区间 insert: 容器中现有的每个元素移动一次
=> 总共: N 次 assignment + N 次 copy ctor
区间 insert
把容器中现有元素 直接移动到它们的最终位置上
前述分析正确的前提: 区间 insert 仅当能确定两个迭代器之间的距离 而不会失去它们的位置
时, 才可以1次就把元素移动到其最终位置上
`前向迭代器` 提供了这样的功能
|\
|
|
`标准容器` 的所有迭代器都提供 `前向迭代器` 功能
`哈希容器` 的迭代器也提供 `前向迭代器` 功能
不提供这一功能
的标准迭代器仅有 输入和输出迭代器
istream_iterator(item6) => 区间 insert必须 一步步移动到其最终位置
输出迭代器 不能用来 标明一个区间
=> 不存在这个问题
[3] 容器 扩容
时, 内存重新分配
及其伴随的 移动/拷贝
问题
插入 N 个新元素
最多可导致 log2 N 次新的内存分配
多数 vector 是2倍扩容
对 string, 上述分析同样有效
对 deque, 内存重新分配 的论断不再适用, 但 元素不必要移动论断仍适用
对 list, 内存重新分配 和 元素移动/拷贝
问题换成了, 某些节点 的 next 和 prev 指针 的 重复冗余赋值
除最后1个新节点之外的新节点的 next 指针, insert 位置节点的 prev 指针
2*(N-1)次 多余的赋值
对 关联容器, 效率问题更加难以说清楚
3 总结
了解`哪些成员函数支持`区间, 知道`何时使用`区间操作 大有好处
下面, iterator 指 容器的迭代器类型 container::iterator
(1) 区间创建
所有标准容器
都提供如下 区间 Ctor
container:container(InputIter beg, InputIter end);
// item29
istream_iterator
或 istreambuf_iterator
作实参时, 避免 C++ 分析(parse) 机制(item6)
编译器解释为 函数声明
(2) 区间插入
所有标准序列容器
都提供 如下区间 insert
void container::insert(iterator insertPos,
InputIter beg, InputIter end);
关联容器 用 比较函数
决定元素该插入何处 => 省去 insertPos 参数
void container::insert(InputIter beg, InputIter end);
循环 + 单元素插入
的其他变体
, 也可能应该替换为 区间 insert
[1] 循环
push_front / push_back
[2] front_inserter / back_inserter 作参数传给 copy
(3) 区间删除
所有 标准容器都提供 区间删除(erase)
序列容器
iterator container::erase(iterator beg, iterator end);
关联容器
void container::erase(iterator beg, iterator end);
返回值 不同
, 为何
有此区别 ?
据说 关联容器版本的 erase `返回`一个迭代器(`指向被删除元素之后的元素`)将导致`不可接受的性能负担`
本条款对 insert 的效率分析对 erase 也类似
有1条对 erase 不适用
vector / string
不足以容纳新元素时, 内存自动增长
但`元素数减少`时, `内存不会自动减少` -> `手动减少`所占多余内存 (item17)
对区间 erase, 注意 erase-remove
习惯用法 (item32)
(4) 区间赋值
void container::assign(InputIter beg, InputIter end);
item6:当心 C++ 编译器
最烦人的 解析(parse)机制
本意是构造匿名临时对象
的实参T(arg)/T()
, 被 编译器 parse 为 T arg/T (*pf)()
的 形参(函数指针)声明
即 构造函数调用 U u( T(arg)/T(arg) ) 被 parse 为函数声明 U u( T arg/T (*pf)(arg) )
Note: C++中1条普遍规律: 尽可能地解释为 函数声明
1 函数声明的 3 种形式
int f(double d);
int f(double (d) ); // 形参名加圆括号 -> 去参数名圆括号: int f(double d);
int f(double); // 省略形参名 -> 补全省略的形参名: int f(double d);
函数指针 作参数
的函数声明
int g(double (*pf) () ); // 形参为 函数指针
int g(double pf() ); // pf 为隐式函数指针: <=> int g(double (*pf)() );
int g(double () ); // 省略形参名 -> 不上省略的形参名 int g(double f() ); <=> int g(double (*pf)() );
函数声明/函数调用 中, 给 形参名/实参名 加圆括号: 合法
=> 可以给参数名加圆括号
2 例1: 文件含整数(int), 复制到 list
ifstream dataStream("ints.data");
list data(istream_iterator(dataStream), // 本意: T(arg) 的 临时对象
istream_iterator() ); // 本意: T() 的 临时对象
<=> 被编译期 parse 为 => 编译期视为函数声明
list data(istream_iterator dataStream ,
istream_iterator pf() );
<=>
list data(istream_iterator dataStream ,
istream_iterator (*pf)() );
3 例2: 构造对象
被 parse 为 函数声明
class Widget{};
Widget w(); // -> parse 为 返回类型为 Widget, 参数为空的 函数(名为 w)声明
4 绕过这种 parse 的方法
(1) 不可移植
的方法, 并非所有编译器能编译通过
给任一 函数实参 整体 加括号
, 强迫编译器 parse 为真正用意
list data( (istream_iterator(dataStream) ), // 给1个实参加括号即可
istream_iterator() );
(2) 通用/更好的方法: 避免使用匿名对象, 给 匿名对象 1个名称
ifstream dataStream("ints.data");
istream_iterator dataBeginIter(dataStream);
istream_iterator dataEndIter();
list data( tempIsIter, tempIsIterEnd);
虽然用命名对象 与通常的 STL 程序风格相违背
, 但为了 避免编译器二义性
, 且使代码易理解
, 这种代价值得
item7:若指针容器
中的指针由 new
操作创建, 容器对象析构前
client 应该 delete 指针
容器很聪明
提供迭代器, 以进行向后和向前的遍历: begin、end、rbegin
告诉你所包含的元素类型: value_type 类型定义
插/删: 自己进行必要的内存管理
报告自己有多少对象, 最多能容纳多少对象: size / max_size
1 容器 自身析构时
, 通过调所含对象的 dtor, 自动析构所包含的每个对象
=> 若指针容器
中的指针由 new
操作创建, 指针容器 析构
时, 自动析构所包含的每个对象(即 指针), 但 指针的 "析构函数" 不做任何事情
=> 不会调用 delete => new 出的对象没有被 delete => 资源泄漏
void f()
{
vector pwVec;
for(int i = 0; i < N; ++I)
pwVec.push_back(new W); // 填充指针
// use pwVec
} // 这里发生 W 泄露
2 delete new 出的对象 是 client 而非容器的的责任
, 只有 client 知道 new 出的对象在容器析构时是否应该被 delete
(1) 能行
但 没 for_each(item43) 清晰
, 且 not 异常安全
的 delete 方法: for 循环
void f()
{
vector pwVec;
// ... 同上
for(vector::iterator iter = pwVec.begin();
iter != pwVec.end();
++iter)
delete *iter; // 删除指针
}
若 填充指针 和 删除指针
2个过程中 抛出异常
, 仍有 资源泄漏
(2) 解决1: delete 操作变成 模板函数对象 DeleteObj
, 作 for_each 第3参数
问题: client 必须指明 DeleteObj 要删除的对象类型
, 这
[1] 应该有办法让编译器推导出
[2] 可能导致 难追踪的错误, 如 通过基类指针 delete 派生类对象, 而 基类没有虚 Dtor
template
struct DeleteObj: public unary_function // item40 解释为什么要有这个继承
{
void operator()(const T* pT) const
{
delete pT;
}
};
void f()
{
// ... 同上
for_each(pwVec.begin(), pwVec.end(), DeleteObj() );
}
问题
class MyString: public string { /* ... */ };
void f()
{
deque pMSDeq;
//...
for_each(pMSDeq.begin(), pMSDeq.end(),
DeleteObj() ); // undefined behavior
}
(3) 解决2: 函数对象 DeleteObj 去模板化去基类, 将模板化移到其函数调用运算符 operator() 中
, 让 编译器
据模板函数实参推断 推断出 DeleteObj::operator()(const T* pT) 要 delete 的 指针类型 const T*
for_each 调用时, 编译器据其 第1实参(迭代器) 推断出其 第3实参 DeleteObject() 的 operator()(const T* pT) 的 指针类型
f(*first) // *first: 指针容器的元素类型 => 指针类型 pT
struct DeleteObj // 去掉模板化和基类
{
template // 加入 模板化
void operator()(const T* pT) const
{
delete pT;
}
};
void f()
{
deque pMSDeq;
//...
for_each(pMSDeq.begin(), pMSDeq.end(),
DeleteObj() ); // 确定行为
}
缺点: 舍弃了
DeleteObj 的可配接能力(item40)
, 但考虑到 DeleteObj 的设计初衷(用于 for_each), 这就不是问题了
直接而类型安全, 但不是异常安全
(4) 解决3: 用 智能指针容器
代替指针容器
通常是基于引用计数
的智能指针
std::shared_ptr
void f()
{
typedef std::shared_ptr SpW;
vector spWVec;
//...
for(int i = 0; i < N; ++I)
spWVec.push_back(SpW(new W) );
// ...
}
(5) 类似的 DeleteArray
指向数组的指针的容器
可避免资源泄露
动态分配的数组几乎总是不如 vector 和string(item13) => 你应该永远用不到 DeleteArray
item8:切勿创建 含 auto_ptr 的容器
- C++标准规定, auto_ptr 容器 (
COAP) 被禁止
, 使用它的代码不会被编译通过
但, 很多 STL平台并没有拒绝COAP
COAP 不可移植
-
复制
auto_ptr 时, 其所指对象的所有权被转移
到复制的 auto_ptr 上, 它自身被置为 NULL
auto_ptr ap1(new W);
auto_ptr ap1(ap2);
ap1 = ap2;
对 auto_ptr 容器 sort
bool wApCmp(const autp_ptr& lhs,
const autp_ptr& rhs)
{
return *lhs < *rhs; // 假设 W 有 operator < 操作符
}
vector > apWVec;
// ...
sort(apWVec.begin(), apWVec.end(),
wApCmp);
sort 实现为 快速排序
思想
容器中的某个元素被当做"基准元素"(pivot element)
大于和小于等于该元素的其他元素 递归调用排序操作
template
void sort(RAIter first, RAIter last, Cmp cmp)
{
typedef typename iterator_traits::value_type ElemType;
RAIter iter;
// iter = pivotElemIter; // 使 iter 指向基准元素
ElemType pivotValue(*iter); // 把基准元素复制到局部临时变量
}
问题
(1) 指向基准元素的 auto_ptr 从容器复制到临时对象 时, 容器中被复制的 auto_ptr 被置为 NULL
(2) 临时对象 scope 结束时, 它会自动删除其所指向的 W
=> sort 返回时, 容器中内容已被改变, 至少1个元素被置为 NULL, 它原来管理的 W 被 delete
- 解决: 用 std::shared_ptr (item50)
item9:慎重选择 删除元素
的方法
0 总结
(1) 删
容器中 有特定值
的所有对象
1) 标准序列容器
vector/string/deque
erase-(非成员)remove
list
[1] 可用: erase-remove
[2] 最优: list::remove
2) 标准关联容器
erase 成员函数
(2) 删容器中 满足特定条件
的所有对象
1) 标准序列容器
`remove 改为 remove_if`
vector/string/deque
erase-(非成员)remove_if
list
[1] 可用: erase-remove_if
[2] 最优: list::remove_if
2) 标准关联容器
[1] 低效: remove_copy_if + c.swap
[2] 最优: `手写循环 来 边遍历容器边删除, 后置递增的迭代器 传给 erase`
(3) 循环内部除了删除对象, 还要 doSomthing
1) 标准连续内存序列容器
手写循环 来 边遍历容器边删除, `用 erase 返回值 更新迭代器`, erase 前后可 doSomething
2) 标准关联容器
手写循环 来 边遍历容器边删除, `后置递增的迭代器 传给 erase`, erase 前后可 doSomething
3) list
上述两种方法均可, 惯例用和 vector 相同的方法
1 删除
标准 STL 容器 c 中所有 等于特定值(1963) 的元素 -> 删除方式随容器类型而异
Container c;
(1) 对 标准序列容器: (成员)erase-(非成员)remove (item32)
; 对 list, 成员 remove 最优(item44)
remove 线性时间
// 对标准 连续内存容器 (vector / deque / string), 最优; 对 list, 次优
c.erase( remove(c.begin(), c.end(), 1963),
c.end() );
// 对 list, 最优
c.remove(1963);
(2) 标准关联容器(set/map/multiset/multimap)
1) 无成员函数 remove
2) 非成员算法 remove
可能
[1] 覆盖容器的值(item32)
[2] 同时 破坏容器 (item22)
对 map/multimap 用 remove 算法, 不能编译
对 set/multiset 用 remove 算法, 可能编译不过
解决: 成员函数 erase
[1] 正确
[2] 高效(对数时间)
[3] 基于等价
(equivalence)而不是相等(equality) (item19)
c.erase(1963);
2 删除 使 条件(predicate, item39) 为 true
的所有元素
(1) 标准序列容器
(vector / string / deque / list) -> remove 改为 remove_if
bool badValue(int x);
// c 是 vector / string / deque 时, 最优方法
c.erase( remove_if(c.begin(), c.end(), badValue),
c.end() );
// c 是 list 时, 最优方法
c.remove_if(badValue);
(2) 关联容器: 2 种方法
[1] 简单低效: remove_copy_if 把需要的值 copy 到新容器
, 再 swap/交换 原容器和新容器 内容
缺点: 要 复制不被删除的元素
到新容器 -> 这个代价可以避免
AssocContainer c;
// ...
AssocContainer goodValues;
remove_copy_if(c.begin(), c.end(),
std::inserter(goodValues, goodValues.end() ),
badValue);
c.swap(goodValues);
[2] 高效: 直接从原始容器中删除元素
, 但是, 关联容器 没有成员函数 remove_if
, 必须 手写循环 来 边遍历容器边删除
1] 立刻想到的代码很少恰好是能工作的代码 -> 多思考一下+修改即可
容器中元素被删除时, (原先)指向被删元素的 迭代器失效
=> c.erase(iter) 返回后, iter 失效, 之后的 ++iter 会导致 undefined behavior
AssocContainer c;
// ...
for(AssocContainer::iterator iter = c.begin();
iter != c.end();
++iter)
{
if(badValue(*iter) )
c.erase(iter); //
}
2] 解决: 删除/erase 之前, 先 递增迭代器(算出 指向下一个元素的迭代器), 再将 迭代器旧值 传给 erase
=> 最简办法: 后置递增, 递增是副作用
AssocContainer c;
// ...
for(AssocContainer::iterator iter = c.begin();
iter != c.end();
) // for 语句第3部分为空
{
if(badValue(*iter) )
c.erase(iter++); // Note: 先 递增迭代器, 再将迭代器旧值传给 erase
else
++iter;
}
<=>
AssocContainer c;
// ...
AssocContainer::iterator tmpIter;
for(AssocContainer::iterator iter = c.begin();
iter != c.end();
)
{
if(badValue(*iter) )
{
tmpIter = iter; // record iter 旧值
++iter; // iter++; // 递增
c.erase(tmpIter); // iter 旧值传给 erase
}
else
++iter;
}
3 每次删除
(满足条件的)元素时, 向日志(log)文件写1条信息
(1) 标准 关联容器
-> 刚才循环中删除前 加1条 log 语句
即可
AssocContainer c;
// ...
for(AssocContainer::iterator iter = c.begin();
iter != c.end(); )
{
if(badValue(*iter) )
{
logFile << "Eraseing " << *i << '\n';
c.erase(iter++);
}
else
++iter;
}
(2) 标准 连续内存 序列容器(vector/string/deque)
[1] 不能再用 erase-remove
原因: 没法使 erase 或 remove 向日志文件写信息
[2] 不能用 为关联容器设计的循环
原因: 对连续内存 序列容器, erase 不仅会使(原先)指向被删除元素的迭代器失效, 还会使被删除元素之后的所有迭代器失效 => undefined behavior
解决: 用 erase 返回值(指向被删元素下一个元素的有效迭代器) 更新迭代器
Note: 对关联容器, erase 返回类型为 void => 不能用该方法
for(ContinuousMemorySeqContainer::iterator iter = c.begin();
iter != c.end(); )
{
if(badValue(*iter) )
{
logFile << "Eraseing " << *i << '\n';
iter = c.erase(iter); // 用 erase 返回值 更新迭代器
}
else
++iter;
}
(3) 对 list 呢 ?
就 遍历和删除
而言, 把 list 当作 标准连续内存序列容器(vector/string/deque) 和 标准关联容器
都可以, 2种方法对 list 都适用。惯例
是对 list 采取和 vector/string/deque 相同的方式
item10:了解 分配子(allocator)
的约定和限制
1 分配子 allocator 设计意图
[1] 提供 内存模型的抽象
-> 目的没达到
[2] 作为对象形式 而存在的内存管理器
-> 但效率降低 -> 降低 作为对象形式 的要求
[3] 像 new / new[] /delete / delete [] 一样, 负责 分配和释放 原始内存
但, 接口 与 new/new[]/malloc 不相似 => 多数标准容器 从不向与之关联的分配子申请内存
2 分配子(allocator) 的约定和限制
(分配子不能用来做什么?)
同一类型的分配子必须等价
, 以使得 一个分配子对象
(比如L2)分配的内存就可以由 另一个分配子对象(比如L1)安全地删除
可移植的分配子对象 不可以有状态(state)
意味着 可移植的分配子 不可以有任何 non-static 的数据成员, 至少不能有会影响其行为的数据成员
一个 SpecialAllocator 从某个堆(heap) 分配, 而另一个不同的 SpecialAllocator 从另一个不同的堆分配
不等价 运行时破坏数据结构
确保指定类型的所有分配子都等价, 这是你的责任
Note: 配子能用来做什么 ? item11
item11:理解 自定义分配子
的合理用法
你用自己的方式测试, 结论是 STL 默认的内存管理器(即 allocator)
[1] 太慢
[2] 或者浪费内存
[3] 或者 在你使用STL的情形下导致了 内存碎片
[4] 或者 你发现 allocator
, 而你使用的是 单线程
, 你不愿为线程同步付出不必要的开销
[5] 或者 某些容器中的对象通常是一起使用
, 你想把它们放在 特殊堆中的 相邻位置
, 以尽可能做到 引用局部化
[6] 或者 你想建立1个 与共享内存 相对应的 特殊堆
, 在其中放 1个或多个容器
, 以 使其他进程 可共享这些容器
则, 你应该自定义分配子
1 例1: 用 malloc 和 free 内存模型来管理 共享内存堆
void* mallocShared(size_t bytes);
void* freeShared(void* ptr);
(1) 创建1个 vector, 其元素位于共享内存
, 但 v 自己 几乎肯定不会位于共享内存
template
class SharedMemAlloc
{
public:
// ...
pointer allocate(size_type objNum, const void* localityHint = 0)
{
return static_cast(mallocShared(objNum* sizeof(T) ) );
}
void deallocate(pointer ptr, size_type objNum)
{
freeShared(ptr);
}
};
typedef vector > SharedMemVec;
// ...
{
//...
SharedMemVec v; // 创建1个 vector, 其元素位于共享内存, 但 v 自己 几乎肯定不会位于共享内存
//...
}
(2) 为了 把 容器的元素 和 容器自身 都放到共享内存
, 用 mallocShare() + placement new 为容器分配空间并构造容器
void* p = mallocShared(sizeof(SharedMemVec) );
SharedMemVec* pVec = new (p) SharedMemVec;
// 用 pv 使用 对象
pv->~SharedMemVec();
freeShared(p);
分配/构造/析构/释放”(allocate/construct/destroy/deallocate)四步曲
2 两个堆 Heap1, Heap2, 都有相应的 静态成员函数 来执行 内存分配和释放
, 想把 某些STL容器的内容(元素) 放在不同的堆中 (vector/set 放 Heap1, list/map 放 Heap2) -> 自定义分配子: 将堆类型模板化, 作第2模板形参; 封装 Heap 的分配/释放函数
class Heap1
{
public:
// ...
static void* alloc(size_t bytes, const void* p);
static void dealloc(void* ptr);
// ...
};
class Heap2 { /* ... */};
template
class HeapAllocator
{
public:
// ...
pointer allocate(size_type objNum, const void* localityHint = 0)
{
return static_cast(Heap::alloc(objNum * sizeof(T) ),
localityHint);
}
void deallocate(pointer ptr, size_type onjNum)
{
Heap::dealloc(ptr);
}
// ...
};
vector > v;
set,
HeapAllocator > s;
list > lst;
map,
HeapAllocator, Heap2> > m;
Note: Heap1 和 Heap2都是类型
若 Heap1和 Heap2 是对象而不是类型 => 违反 item10 中 同一类型的分配子必须等价
item12:切勿对 STL容器的线程安全性
有不切实际的依赖
0 总结
容器的线程安全性 无法单纯地
靠 容器自身单个成员函数
, 或 操作容器的单个算法
保证, 这没有意义;
而是应该由用户去保证, 用户 操作容器某段区间期间, 必须全程持有锁
, 以保证在此期间 用户当前线程看到的一直是自己正在处理的那个容器, 而该容器没有被其它线程改变
1 对一个STL实现你最多只能期望
(但不能依赖)
(1) 多个线程 读
是安全的
(2) 多个线程 对不同的容器写
是安全的
2 1个库试图实现 完全的容器线程安全性
, 则可能采取的方式
(1) 容器成员函数的每次调用
, 都锁住容器
直到调用结束
(2) 容器返回的每个迭代器
(如 通过 begin()/end() 调用)的 生存期结束前
, 都锁住容器
(3) 作用于容器
的 算法
, 都锁住容器
, 直到算法结束
这实际上 没有意义
, 因为, 算法无法知道它们所操作的容器(item32)
即便 算法能知道
它们所操作的容器(item32), 这种做法 仍不能实现线程安全性
3 例1: vector 中查找值为5的第1个元素, 找到则置为0
vector v;
// ...
vector::iterator first5( find(v.begin(), v.end(), 5) ); // 第1行
if(first5 != v.end() )
{
*first5 = 0;
}
多线程下, 另一个线程可能会夹在1、2行中间 或 2、3行中间, 使 first5 无效
[1] 插入
, 使 vector 重新分配内存(item14)
=> vector 所有迭代器无效 => 1、2 行看到的容器(空间)不同
[2] 删除
, 使 first5 无效 => 2、3 行看到的容器不同( first5 上的元素变了)
上面的加锁方式都不能防止这类问题的发生
begin() 和 end() 的调用 返回得太快, 生成的迭代器 的生存期直到该行结束;
find也在该行结束时返回
想做到 线程安全
: 必须手工做同步控制
, 第1行到第3行始终持有锁
vector v;
// ...
getMutexFor(v);
vector::iterator first5( find(v.begin(), v.end(), 5) ); // 第1行
if(first5 != v.end() )
{
*first5 = 0;
}
releaseMutexFor(v);
更 OO 的方法: RAII
-> 也 异常安全: 局部对象总是会被析构
template
class Lock
{
private:
const Container& c;
public:
Lock(const Container& c_)
: c(c_)
{
getMutexFor(c);
}
~Lock()
{
releaseMutexFor(c);
}
};
vector v;
// ...
{
Lock >lock(v);
vector::iterator first5( find(v.begin(), v.end(), 5) ); // 第1行
if(first5 != v.end() )
{
*first5 = 0;
}
} // 代码块结束, 自动释放 mutex