Effective STL-1 容器

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 优于 string 的case: item13

(2) vector 时空都优于 标准关联容器 的case: item23

(3) "数组优于STL容器" 的 case: item16

(4) bitset 优于 vector 的 case: item18

数组也可以被用于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 前端插入 N个元素

[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 的容器

  1. C++标准规定, auto_ptr 容器 (COAP) 被禁止, 使用它的代码不会被编译通过
    但, 很多 STL平台并没有拒绝COAP

COAP 不可移植

  1. 复制 auto_ptr 时, 其所指对象的所有权被转移到复制的 auto_ptr 上, 它自身被置为 NULL
    auto_ptr ap1(new W);
    auto_ptr ap1(ap2);
    
    ap1 = ap2;
  1. 对 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

  1. 解决: 用 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 

你可能感兴趣的:(Effective STL-1 容器)