本文发自 http://www.binss.me/blog/my-excerpt-of-effective-stl/,转载请注明出处。

接下来是看《Effective STL》。依然还是受益匪浅,然而也有许多未理解之处,期望日后第二遍时有更深入的理解。

以下是读书过程中的摘录。

引言

凡是涉及指针容器的的设计几乎无一例外地会用到引用计数。

所有重载了operator() 的类都是 函数子 类(functor class)。从这些类创建的对象被称为函数对象或函数子,因为它们可以像函数那样被调用。

模版的嵌套从属类型前需要加typename(避免被当作静态成员)。

第一章 容器

1 慎重选择容器类型

标准序列容器:vector、string、deque、list

标准关联容器:set、multiset、map、multimap

非标准序列容器:slist(单向列表)、rope(重型string)

非标准关联容器:hash_set、hash_multiset、hash_map、hash_multimap

非STL容器:数组、bitset、valarray、stack、queue、priority_queue

连续内存的容器:vector、deque、string、rope

基于节点的容器:list、slist、标准关联容器、散列容器

在任意位置插入新元素:序列容器

元素无序:散列容器

随机访问迭代器:vector、deque、string、rope

避免插入删除时移动元素:别使用连续内存容器

数据布局和C兼容:vector

关注查找速度:散列容器->排序的vector->标准关联容器

避免引用计数:别使用string、rope

可回滚:基于节点的容器。只有list支持多个元素操作的回滚。

避免迭代器、指针、引用无效:基于节点的容器

迭代器随机访问,若没有删除且插入只发生在容器末尾,则指针和引用不会失效(但迭代器可能会失效):deque

2 不要试图编写独立于容器类型的代码

不要妄想更换容器后代码无须改变:

  • 专有操作:比如序列容器才支持push_front和push_back,关联容器才支持count和lower_bound

  • 操作因容器不同而不同:如insert和erase

  • 只能使用功能的交集而无法使用特性

  • 迭代器、指针、引用失效时机不同

为了方便日后更换容器(或分配子)、缩短类型名称的长度,可以用typedef为容器和其迭代器定别名。

为了避免容器暴露,还可以用类对容器进行封装。

3 确保容器中的对象副本正确而高效

对象存入容器中时会被复制,被取出时也会被复制(copy in, copy out)。

在进行排序等操作时,容器中的对象也会被复制(移动)。

不要将子类元素放入基类容器中,不仅不能利用虚特性,还导致对象被切割。

为了高效,可以考虑智能指针。

STL仅在需要时才创建对象,比数组聪明的多。

4 调用empty而不是检查size()是否为0

empty对所有的标准容器都耗费常数时间,而对于一些list实现,size耗费线性时间。

这些list实现追求让操作(如splice)耗费常数时间,不维护链表的大小,而是直到调用size()时才进行计算。否则,splice操作为了计算所链接的元素个数,需要耗费线性时间。

5 区间成员函数优先于与之对应的单元素成员函数

简单来说,能一次拷多个的就不要一个个拷,因为一个个拷效率低。也不要用alogirithm的copy+插入迭代器来拷,因为copy往往不能准确地表达语义,并且效率和一个个拷差不多。

注:copy的原型:

template<class InputIterator, class OutputIterator>
OutputIterator copy(InputIterator _First, InputIterator _Last, OutputIterator _DestBeg);

区间函数列表:

  • 区间创建

    container::container(InputIterator begin, InputIterator end);

  • 区间插入

    序列容器:void container::insert(iterator position, InputIterator begin, InputIterator end);

    关联容器:void container::insert(InputIterator begin, InputIterator end);

  • 区间删除

    序列容器:iterator container::erase(iterator begin, iterator end);

    关联容器:void container::erase(iterator begin, iterator end);

  • 区间赋值

    void container::assign(InputIterator begin, InputIterator end);

区间成员函数的优势:

  • 意图清晰
  • 代码精简
  • 效率更高

假设容器中有n个元素,现新插入k个元素。

减少了不必要的函数调用:区间成员函数只需调用一次;而单元素成员函数要调用k次。

减少了容器中元素的移动开销:区间成员函数只需移动一次,带来n次赋值操作符函数的调用和k次复制构造函数的调用的开销;而单元素成员函数要移动k次,带来( n-1)*k次赋值操作符函数的调用和k次复制构造函数的调用的开销。

减少了容器扩展开销:基于连续内存的容器在容量不足进行扩展时会将容量加倍,区间成员函数能够一次到位;而单元素成员函数要扩展log2(k)次。

基于节点的容器虽然没有移动和扩展开销,但相应的,会带来2*(k-1)次不必要的节点next和prev指针赋值开销。

6 当心C++编译器最烦人的分析机制

C++会尽可能地解释为函数声明而非对象声明。

list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>()); 会被解析成一个data函数。

为了避免,只能使用命名的迭代器:

istream_iterator<int> dataBegin(dataFile);

istream_iterator<int> dataEnd;

list<int> data(dataBegin, dataEnd);

7 如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉

使用智能指针容器(容器中的对象为智能指针,且要求为引用计数形式的,如std::shared_ptr),保证容器中new出来的资源总能被释放。

8 切勿创建包含auto_ptr的容器对象

复制auto_ptr时,它所指对象的所有权会移交给新的指针。这样在容器中会导致严重后果:

对于vector sort()的一种实现——快排,在选轴(pivot)时会把对象的所有权转移到临时变量(pivot用于比较),然后临时变量在sort()结束时被释放,导致vector中的智能指针丢失对象(变为NULL)。

注:auto_ptr已在C++11中废弃。见 http://stackoverflow.com/questions/3697686/why-is-auto-ptr-being-deprecated

9 慎重选择删除元素的方法

没有对所有容器都适用的删除元素方法。

基于连续内存的容器:采用erase-remove

c.erase(remove(c.begin(), c.end(), deleteValue), c.end());
c.erase(remove_if(c.begin(), c.end(), deleteValuePredicate), c.end())

list:

c.remove(deleteValue);
c.remove_if(deleteValuePredicate);

for(Container<ValueType>::i=c.begin(); i != c.end();)
{
    if(deleteValuePredicate(*i))
    {
        i = c.erase(i);
    }
    else
    {
        ++i;
    }
}

10 了解分配子(allocator)的约定和限制

STL内存分配子是一个模版,用于分配和释放原始内存。

声明:

template<typename T>
{
public:
    pointer allocate(size_type numOjbects, const void *localityHint=0);
    void deallocate(pointer ptrToMemory, size_type numOjbects);
    ...
};

模版参数T代表其分配内存对象的类型。它应该提供类型定义pointer(T*)和reference(T&)。

对于pointer(T *),即使T对象未进行构造,但是却返回了一个T的指针。STL期望调用者会在返回内存中构造T对象,然而在reserve情况下可能并不会发生构造。

C++标准允许STL的实现假定同一类型的分配子是等价的,因此可移植的分配子都不能拥有非静态的数据成员。

标准容器依赖于rebind模版,因此分配子必须提供rebind模版。

11 理解自定义分配子的合理用法

如果STL默认的分配子有新要求,可以自定义分配子,然后在构造容器时作为第二个模版参数传入。如:

vector<double, SharedMemoryAllocator<double>> SharedDoubleVec;

注意同类型的分配子必须等价。

主要需求:

  • 使用共享内存

  • 将不同容器的对象放置于不同的堆

12 切勿对STL容器的线程安全性有不切实际的依赖

STL标准只能保证:

  • 多个线程读是安全的

  • 多个线程对不同容器的写是安全的

为了实现线程安全,在容器的成员函数调用结束前、容器返回的迭代器生存期结束前、作用于容器的算法结束前,都要锁住容器。

有时还需要对多行代码进行同时加锁,STL很难推断出来。

加锁开销大,如果STL都加锁,对无须线程安全的用户不公平。

应该通过加锁类来创建容器,这样锁在容器作用域结束时随着析构自动释放。为了使加锁类发挥作用,创建新的代码块来括住要加锁的代码。

第二章 vector和string

13 vector和string优先于动态分配的数组

使用new需要确保:必须调用且只能调用一次delete(否则会资源泄漏),且delete形式正确(delete还是delete[])

vector和string消除了这些负担,且提供了begin、end、size这些方便的函数以及迭代器。因此考虑用vector和string来替代new T[...]。

string一般实现为引用计数,如果用于多线程环境,可以考虑用vector<char>取代。

14 使用reserve来避免不必要的重新分配

每当vector和string需要更多空间时,就会执行以下步骤:

  1. 分配一块大小为当前容量n倍的内存(一般n为2)

  2. 将原容器的内容从旧内存拷贝到新的内存中

  3. 析构旧内存中的对象

  4. 释放旧内存

为了避免频繁的重新分配,可以通过reserve(Container::size_type n)来扩大容器容量。

这种做法适用于预先能估算容器元素数量的情况;也可以先预留,在元素加入完后再去除多余容量。

15 注意string实现的多样性

一般来说,string的实现包含以下信息:字符串的大小、容量、字符串的值。还可能包含分配子的一个副本、值的引用计数等。

实现一

包含字符串的大小、容量、和分配子的副本,还有一个指向动态分配内存的指针,内存中包含引用计数和字符串值。

实现二

只包含指向struct的指针。struct包含了字符串的大小、容量、引用计数、指向值的指针,以及同步控制相关的其它数据。

实现三

同实现二,只不过指针指向了一块动态分配的内存。不支持针对单个对象的分配子。

实现四

内部包含一块内存,可以容纳15个字符(小字符串优化)。当字符串大小超过15时,内存的起始部分被替换为指向动态分配内存的指针,指向字符串的值。

为了有效使用STL,应该了解string的实现。

16 了解如何把vector和string数据传给旧API

vector中的元素存储在连续的内存中,所以能过直接把vector传给接受数组参数的API:

if(!v.empty())
{
    doSomething(&v[0], v.size());
}

利用数组来初始化vector:

size_t fillArray(double * pArray, size_t arraySize);
vector<double> vd(maxNumDoubles);
vd.resize(fillArray(&vd[0], vd.size()));

fillArray用于向vector中写入double元素,并返回写入成功的个数。

因为只有vector有这种特性,可以让其作为容器和C API之间的中介,如:

利用vector初始化list:

list<double> l(vd.begin(), vd.end());

利用set初始化vector:

vector<int> v(intSet.begein(), intSet.end());
if (!v.empty())
{
    doSomething(&v[0], v.size());
}

注意:这种取指针的方式通常(非排序)可以修改vector中的元素,但不能增加或删除vector中的元素。

17 使用swap技巧除去多余的容量

容器容量只会自动增加,不会自动减少。通过swap可以缩简容量:

vector<Contestant>(contestants).swap(contestants);

vector<Contestant>(contestants)会调用拷贝构造函数创建出临时对象,而拷贝复制函数只会为存在的元素分配内存,因此临时容器对象没有多余容量。然后通过swap交换两者的内容(当然迭代器、指针、引用也会被交换),于是原本容器中的内存随着临时变量的析构而释放了。

swap也可以用来清空容器:

vector<Contestant>().swap(contestants);

18 避免使用vector<bool>

vector<bool>并不存储bool,它无法完全满足STL容器的需求,不是一个STL容器。它是通过bitfield来实现的,用一个bit代表一个bool值,因此无法获取其中单独的一个元素对象。所以为了保持和其它容器行为上的一致,operator []返回代理而不是引用,导致对其返回值取址得到的不是一个bool指针,违反了STL的的要求。

可以使用deque<bool>和bitset(非STL)取代之。

第三章 关联容器

19 理解相等(equality)和等价(equivalence)的区别

相等:以operator ==为标准。只要operator ==返回true,不管内部是否相同,都表示相等。

等价:以"在排序的区间中对象值的相对顺序“为标准。

关联容器基于等价而非相等,始终使用判别式来排序、判断元素是否“相等”:

!c.key_comp()(x, y) && !c.key_comp()(y, x)

key_comp的默认实现为less,即“<“。可以自定义(通过函数子类,struct继承某个函数子)来实现:

struct StringCompare: public binary_function<string, string, bool>
{
    bool operator()(const string& lhs, const string& rhs) const
    {
        return my_compare(lhs, rhs);
    }
};

并作为第二个模版参数传入:

set<string, StringCompare> my_set;

set的声明如下:

template <class Key, class Compare = less<Key>, class Allocator = allocator<Key> >
class set;

20 为包含指针的关联容器指定比较类型

关联容器存放指针时,要想其按照内容排序(而不是按指针排序),应该自己实现比较函数子类(见#19),并作为第二个模版参数传入。

为了方便,可以创建比较函数子类模版:

struct DereferenceLess
{
    template<typename PtrType>
    bool operator()(PtrType pT1, PtrType pT2) const
    {
        return *pT1 < *pT2;
    }
};

21 总是让比较函数在等值情况下返回false

在自定义比较函数子类时,可能导致在等值情况下比较函数子返回true(如>=),于是集合允许两个等值的元素插入到容器中(判断为不等价),违反了标准关联容器(如set等)的定义。

对关联容器排序的比较函数必须为它们所比较的元素定义一个“严格的弱序化”,其中一个要求就是:总是在比较等值的两个元素的时候返回false。

22 切勿直接修改set或multiset中的键

对于map和multimap,其中的元素类型为pair<const K, V>,所以键无法被修改。而set或multiset中的元素类型却是T而不是const T,因为元素的某些成员需要能够被修改。因此需要保证元素的键成员不要被修改,因为它们影响了容器的有序性。

在某些实现中,set或multiset中的元素不允许被修改。不要使用const_cast来去除const元素属性,因为在这种情况下键可能会被修改。

安全的修改步骤:

  1. 用迭代器找出要修改的容器元素
  2. 为该元素复制一份非const副本
  3. 修改该副本
  4. 把元素从容器中删除(erase)
  5. 把元素副本插入到容器中(insert,可以传入1得到的迭代器作为提示)

23 考虑用排序的vector替代关联容器

若对查找速度要求高,可以采用散列容器(几乎为常数时间的查找,视散列函数)。

若对容器操作行为规律(始终按照批量设置->批量查找),可以采用排序的vector。相比标准关联容器,避免了二叉树指针空间开销(占用过多的内存会导致换页,产生page fault),并且由于其节点散布在全部内存空间中,降低了引用局限性。查找时间都是O(log2n)。

但是这种使用方法必须让vector保持有序,因此仅在操作行为规律,即模式为批量设置然后批量查找时采用。

而由于标准关联容器通常被实现为平衡二叉树,更适合增删查操作混杂时使用。

24 当效率至关重要时,请在map::operator[]与map::insert之间谨慎做出选择

map的operator []:先检查key是否在map中,如果是,返回其引用,如果否,构造一个新对象并返回它的引用。

在为map插入元素时,使用insert比operator[]后赋值效率高,因为可以直接调用构造函数而不是先调用默认构造函数再赋值。

但如果是修改元素,则operator[]效率更高,因为insert必须构造pair。

结合operator[]和insert的优点:

template<typename MapType, typename KeyArgType, typename ValueArgType>
typename MapTypee::iterator efficientAddOrUpdate(MapType& m, const KeyArgType& k, const ValueArgType& v)
{
    // lower_bound返回元素位置,如果元素不存在,则返回它应该在(插入到)什么位置
    typename MapType::iterator lb = m.lower_bound(k);
    if( lb != m.end() && !(m.key_comp()(k, lb->first)) )
    {
        lb->second = v;
        return lb;
    }
    else
    {
        typedef typename MapType::value_type MVT;
        return m.insert(lb, MVT(k,v));
    }
}

使用KeyArgType、而ValueArgType不是MapType::key_key、MapType::value_type,是为了避免对象因需要转换而被构造,在赋值情况下,直接调用类的operator = 赋值。

25 熟悉非标准的散列容器

STL没有散列容器。可以熟悉非标准版本的散列容器。

注:C++11引入散列容器std::unordered_map

第四章 迭代器

26 iterator优先于const_iterator、reverse_iterator以及const_reverse_iterator

STL为所有标准容器都提供了4种迭代器:

iterator:相当于T*

const_iterator:相当于const T*

reverse_iterator:相当于T*,从容器尾部反向遍历到头部

const_reverse_iterator:相当于const T*,从容器尾部反向遍历到头部

iterator是基类,const_iterator和reverse_iterator继承了iterator,const_reverse_iterator继承了const_iterator和reverse_iterator。

尽量使用iterator、避免混用的原因:

  1. 大部分版本的insert和erase函数要求使用iterator。

  2. 无法将const_iterator隐式转换为iterator

  3. 将reverse_iterator转换为iterator后要经过调整才能用

27 使用distance和advance将容器的const_iterator转换成iterator

不存在从const_iterator到iterator的隐式转换,也无法通过const_cast进行强制类型转换(除了vector和string,因为它们的迭代器是指针)。

因此将容器的const_iterator转换成iterator的最好方法是复制一个:

Container d;
Container::const_iterator ci;
Container::iterator i(d.begin());
advance(i, distance<Container::const_iterator>(i, ci));

28 正确理解由reverse_iterator的base()成员函数所产生的iterator的用法

调用reverse_iterator的base()成员函数可以得到与之相对应的iterator,对于插入操作,可以直接用,而对于删除操作,reverse_iterator调用base()返回的是下一个位置,应该使用(++ri).base()。

29 对于逐个字符的输入请考虑使用istreambuf_iterator

使用istream_iterator<char>来复制文本内容到string中,要清除输入流的skipws标志:

inputFile.unsetf(ios::skiwps);

但istream_iterator内部调用opertaor>>来进行格式化输入,带来了类型检查、异常检查等开销。

如果单纯进行逐字符拷贝,可以使用istreambuf_iterator<char>,该迭代器只会简单地从流缓冲区中取出下一个字符。

相比来说,istreambuf_iterator比istream_iterator具有更优越的性能。

第五章 算法

30 确保目标足够大

transform:将一个容器中符合条件的元素复制到另外一个容器

使用transform算法时,因为默认进行的是替换型操作,必须确保目标容器相应的元素存在(即目标容器应该大于等于原容器,如果不够可以用resize调整)。

为了将transform的结果插入到目标容器尾端,需要传入back_inserter生成迭代器(需要容器支持push_back):

transform(values.begin(), values.end(), back_inserter(results), transmogrify);

同理,front_inserter能将transform的结果插入到目标容器前端(需要容器支持push_front)。

inserter则能过将结果插入到特定位置,如插入到中间:

transform(values.begin(), values.end(), inserter(results, result.begin() + result.size()/2), transmogrify);

31 了解各种与排序有关的选择

完全排序:

template<class ForwardIt, class UnaryPredicate>
ForwardIt partition( ForwardIt first, ForwardIt last, UnaryPredicate p );

找到前n并排序:

template<class RandomIt, class Compare>
void partial_sort( RandomIt first, RandomIt middle, RandomIt last, Compare comp );

找到前n:

template<class RandomIt, class Compare>
void nth_element( RandomIt first, RandomIt nth, RandomIt last, Compare comp );

sort、partial_sort、nth_element都属于非稳定排序,即在排列等价元素的时候,无法保证原来在前面的排完序依然在前面。

稳定排序:

template<class RandomIt, class Compare>
void stable_sort( RandomIt first, RandomIt last, Compare comp );

因为sort、partial_sort、nth_element、stable_sort需要随机访问迭代器,所以只能应用于vector、string、deque和数组。

满足函数UnaryPredicate(返回bool)的元素会被移到容器前端,返回指向最后一个满足条件元素的下一个元素的迭代器:

template<class ForwardIt, class UnaryPredicate>
ForwardIt partition( ForwardIt first, ForwardIt last, UnaryPredicate p );

partition和stable_partition只需要双向迭代器,所以可以应用于所有标准序列容器。

32 如果确实需要删除元素,则需要在remove这一类算法之后调用erase

无法从迭代器推知对应的容器类型。

remove算法不是真正意义上的删除,它不能从容器中删除元素,

实际上,remove将“不用被删除”的元素移动到区间前部,并返回指向最后一个“不用被删除”元素的下一个元素。

所以在remove后应该调用erase,如删除值为99的元素:

v.erase(remove(v.begin(), v.end(), 99), v.end());

与之相似的还有remove_if和unique。

33 对包含指针的容器使用remove这一类算法时要特别小心

通过remove(remove_if、unique)删除元素时,若元素是指针,则指针会因为被覆盖导致资源泄漏。

解决方法:

  • 调用remove前先遍历容器,如果指针所指的元素要被删除,析构指针所指对象,保留空指针
  • 使用智能指针

34 了解哪些算法要求使用排序的区间作为参数

要求排序区间的STL算法:

为了使用二分查找:binary_search、lower_bound、upper_bound、equal_range、

为了提高性能(保证线性时间):set_union、set_intersection、set_difference、set_symmetric_difference、megre、inplace_merge、includes

不一定要排序区间(但排好序更好):unique、unique_copy

如果容器的排序方式和算法的默认排序方式less(<)不同时,需要指定函数子,如:binary(v.begin(), v.end(), 5, greater<int>());

35 通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较

tolower的参数和返回值都是int,且规定除了EOF外(用负数表示),其他值必须能用unsigned char来表示。

lexicographical_compare可以根据传入函数(判别式)来进行容器内容比较,返回两个容器不同的第一个位置。

不区分大小写字符串比较;

bool ciCharLess(char c1, char c2)
{
    return tolower(static_cast<unsigned char(c1)) < tolower(static_cast<unsigned char(c2));
}
bool ciStringCompare(const string& s1, const string& s2)
{
    return lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);
}

如果sting中间不包含空字符且不考虑国际化支持,可以把string专为 const char* 后用strcmp比较。

36 理解copy_if算法的正确实现

STL中没有copy_if( 注:C++11已加入 ),在此之前,只能手动复制:

while(begin != end)
{
    if( p(*begin) )
    {
        *destBegin ++ = *begin;
    }
    ++begin;
}

37 使用accumulate或者for_each进行区间统计

统计区间元素数目:count

统计区间满足判别式元素数目:count_if

区间最小值:min_element

区间最大值:max_element

自定义统计:

方法一:

template<class InputIt, class T, class BinaryOperation>
T accumulate(InputIt first, InputIt last, T init, BinaryOperation op);

init为统计初始值。op为统计函数,接受两个参数。op不能使任何迭代器失效,也不能修改范围内的任何一个元素(C++11)。

template<class InputIt, class T>
T accumulate(InputIt first, InputIt last, T init);

特化版。统计函数为operator+。即以init为初始值进行累加。

方法二:

template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f);

f可以是统计函数,但只接受一个参数。在for_each执行完毕后,f的副本会被返回,我们必须从中提取我们所需的统计信息。

第六章 函数子、函数子类、函数及其它

38 遵循按值传递的原则来设计函数子类

对于函数(指针)和函数子,作为STL的参数和返回值都是按值传递的。

如果通过模版参数来强制指定为引用,则可能编译不通过(出于性能考虑,有些STL配接器和算法使用引用参数来接受函数子,这样做会造成引用的引用)。

为了避免函数子(对象)的巨大复制开销和子类向父类传参导致的切割问题,将函数子中的数据和虚函数放到一个新的类中,函数子只保留一个指向新类对象的指针(桥接模式)。

39 确保判别式是“纯函数”

判别式(predicate)是一个返回值为bool类型(或可隐式转换为bool)的函数。STL中常用于关联容器的比较函数以及排序算法的参数。

纯函数:返回值仅仅依赖于其参数的函数(当然还有常量)。

判别式类:函数子类,operator ()是判别式。

如果判别式不是纯函数,则其必定依赖于其它某些变量,如函数子的类成员、函数的static变量等。这样会导致判别式拥有状态,影响下次调用的结果。

由于STL中接受判别式的地方既可以接受判别式,也可以接受判别式对象。

40 若是一个函数子,则应使他可配接

STL函数配接器:能够将函数子与另一个函数子、或某一个一般函数、或某个值结合起来形成一个新的函数子,预定义由四个:not1、not2、bind1st、bind2nd。

函数配接器都要求参数可配接,即包含特殊的类型定义:argument_type、first_argument_type、second_argument_type、result_type。

ptr_fun:将普通函数转换为可配接

对于函数子,获取这些特殊类型定义的最好办法是继承以下基类:

template <class ArgumentType, class ResultType>
struct unary_function;
template <class ArgumentType1, class ArgumentType2, class ResultType>
struct binary_function;

尽量不要让函数子拥有多个operator(),因为函数子最多只能继承一个模版实例化的基类,因此只有一种调用形式是可配接的。

注:C++11中,bind1st和bind2nd被废弃。由更泛化的std::bind取代。

ptr_fun被废弃。由更泛化的std::function和std::ref取代。

unary_function和binary_function被废弃。可能是因为C++11引入了更泛化的可变参数模版std::function的原因?

41 理解ptr_fun、mem_fun和mem_fun_ref的来由

mem_fun:用于指针容器对象调整成员函数调用为普通的函数调用( obj->function() -> function(obj) )。函数返回配接器mem_fun_t,该函数子实现调用利用参数调用成员函数。

mem_fun_ref:用于容器对象调整成员函数调用为普通的函数调用( obj.function() -> function(obj) )。函数返回配接器mem_fun_ref_t,该函数子实现调用利用参数调用成员函数。

注:C++11中,mem_fun和mem_fun_ref被废弃。由更泛化的mem_fn和bind取代。

42 确保less<T>operator<具有相同的语义

程序员可以针对用户定义的类型,特化std命名空间内的模版,但不能修改std命名空间中的组件。

less默认调用operator <来完成比较,为了避免违反直觉,不要修改less的行为。

第七章 在程序中使用STL

43 算法调用优先于手写的循环

每一个算法都至少需要使用一对迭代器来指定一个区间,然后在该区间上执行操作。

算法优于手写循环的原因:

  • 效率高(STL的设计者比使用者更了解其内部结构并进行相应的优化,能够采用更高效的算法)
  • 正确性高(更不容易出错)
  • 可维护性强(更简短、语义上更清晰)

44 容器的成员函数优于同名的算法

有些STL容器提供了与算法同名的成员函数,原因:

  • 成员函数速度更快(对于关联容器,对数时间对比线性时间)
  • 成员函数和容器的一贯行为保持一致(对于关联容器,采取等价而不是等值比较;对于map,只比较键而不是全部)

45 正确区分count、find、binary_search、lower_bound、upper_bound和equal_range

线性时间、相等性判断:

  • count:用作存在性测试,统计数目
  • find:用作存在性测试,返回第一个匹配位置,比count效率更高。因为一旦匹配到立刻返回(如果没找到,指向end)

对数时间(需排好序)、等价性判断:

  • binary_search:仅用作存在性判断(只返回bool,表示是否存在)
  • lower_bound:用作存在性判断,返回匹配的第一个位置(如果没找到,指向第一个适合插入的位置),所以需要检查返回位置的值是否与目标值等价,要手工实现等价
  • upper_bound:同lower_bound(如果没找到,指向最后一个适合插入的位置)
  • equal_range:用作存在性判断,返回一对迭代器,第一个为lower_bound返回值,第二个为upper_bound返回值。通过两者返回值是否相等来判断存在,通过两者距离来统计数目(distance)。

46 考虑使用函数对象而不是函数作为STL算法的参数

使用函数子(函数对象)的效率比函数高。

编译器将函数子模版在实例化过程中将operator()内联,避免了函数调用。此外,在实例化的过程中进行了一些正确性检查,减少了运行期出错的可能。

而函数由于通过函数指针来传参,每次调用时都产生一次间址访问开销。受到函数指针的抑制,即使函数被声明为inline,编译器总是不会进行优化。

47 避免产生“直写型”的代码

在一行中调用多个算法的复杂语句被称为直写型代码,这种代码虽然很容易便携,但难以阅读和理解。

应该将复杂语句分解成容易理解的简单语句,并适当地加上一些注释。

48 总是包含(#include)正确的头文件

C++没有规定STL中头文件的相互包含关系,不同的实现中包含方式可能不同,基于可移植性,应该包含全部依赖的头文件,而不依赖于它们之间的相互包含。

几乎所有标准STL容器都被声明在与之同名的头文件中。

除了accumulate、inner_product、adjacent_difference、partial_sum在<numeric>中外,其它算法都被声明在<algorithm>中。

特殊类型的迭代器(如istream_iterator、istreambuf_iterator)被声明在<iterator>中。

标准函数子(如less<T>)和函数子配接器(如not1)被声明在头文件<functional>中。

49 学会分析与STL相关的编译器诊断信息

STL相关语句在编译错误时会抛出一大堆乱七八糟的诊断信息。

简化诊断信息:

  • 对于string的诊断信息,可将basic_string相关的长字符串替换成string。
  • 对于内部类(下划线开头),可以将其模版内容删除掉。
  • 由于vector和string的迭代器是指针,因此可将指针替换为迭代器。

50 熟悉与STL相关的Web站点

SGI:为STL所有组建提供了详尽的文档。有些实现超出了STL范畴。

STLport:提供可在超过20种编译器上使用的SGISTL改进版本,并提供调试模式(用法错误时抛出错误)。

Boost:提供了大量的C++库,并称为STL标准的候补。简称干儿子。