计算机 / 读书笔记 · 2021年12月9日 0

Effective STL读书笔记

虽然书有点老了,不代表所有内容都过时吧。。

Item 3 保存在容器中的类要注意拷贝相关的问题

容器会对保存在其中的对象进行拷贝操作,

  1. 容器中的对象应该小巧,避免拷贝带来的性能损耗;
  2. 容器中的对象应该正确实现拷贝构造函数与拷贝赋值函数;
  3. 把子类放进保存父类的容器里,容器只会调用父类的相关拷贝函数,因此子类的信息全部丢掉了;

Item 4 使用empty()而不是size()==0来判断容器是否非空

按照书中的说法,list可能为了splice()的常数时间复杂度实现,导致size()函数的时间复杂度是线性的。
不过按照cppreference网站的说明,c++11已经要求size()函数的实现是常数时间的了。因此c++11中,splice()函数实现不再要求常数时间实现了。

Item 5 Prefer range member functions to their single-element counterparts

与其自己写个循环来对每一个元素进行操作,不如直接调用现成的按range进行处理的stl函数。原因列举如下:

  • 自己可能写循环时犯错;
  • 调用现成的按range处理的stl函数可能代码更紧凑,看起来更直白;
  • 现成的按range处理的stl函数可能做了你懒得做的优化,比如stl函数里可能就不会在循环的每次判断里比较迭代器是否等于xx.end(),stl函数的实现可能就实现把这个end()迭代器的值给存了起来,不用每次都调用end()函数去获取;
  • 现成的按range处理的stl函数可能根据容器的内部数据结构做了你不能做的、没想到的优化。比如从list中移除一些相邻的元素,自己写循环可能要一个一个的移除,然后每次移除的时候就要把该节点的前后节点再重新连起来,而stl的实现可能就直接把这一段链表给直接移除了,少了移除每个元素时连接前后节点的操作。

最后书中总结了一些所谓的range函数有哪些:

  • range construction
  • range insertion
  • range erasure
  • range assignment

Item 6 Be alert for C++’s most vexing parse

C++里面有很多容易混淆的地方,比如简单的例子:

#include <iostream>
using namespace std;

class A {
public:
	A(){ cout << "constructor" << endl; }
};

int main(int argc, char **argv)
{
	A a();
	return 0;
}

这个例子里看似用A a()声明了一个A类型的变量a,可其实是声明了一个返回类型为A的函数。书中举的复杂例子是:

ifstream dataFile("inst.dat");
list<int> dat(istream_iterator<int>(dataFile),
			istream_iterator<int>());

上面的代码看起来可以用来初始化dat,但是实际上声明了一个dat函数,istream_iterator<int>(dataFile)被当成了这个函数的第一个参数。
这里面涉及到的另一个知识点是函数的参数是可以用括号括起来的,比如int f(double (d))

为了解决上面的问题,书中提出的解决方案是把参数用括号括起来,

ifstream dataFile("inst.dat");
list<int> dat((istream_iterator<int>(dataFile)),
			istream_iterator<int>());

可以这样做的原因是在声明函数的时候,可以给参数名加上圆括号,但是却不能给整个带类型的参数加上圆括号,于是上面的代码就不能被解释为一个函数的声明了,而只能被编译器解析为list的构造函数的调用。

这里还涉及到一个知识点,那就是出现这种问题的原因在于C++总是会尽可能的把语句解析为函数的声明。

Item 7 保存指针的容器在销毁时不会释放指针指向的内存

这个观点其实是显而易见的事实。书中提出的一个小技巧倒是比较有用,那就是在设计函数对象的时候,不要把函数对象的类实现成模板,而是把函数对象里面的()函数实现成模板,这样就可以让编译器自动推导类型参数,而不用在代码里具体指明模板的类型参数了。

Item 8 禁止在容器里放置auto_ptr

auto_ptr已经成为历史,而原因在于容器里不能放置auto_ptr。好吧,在我来得及熟练使用auto_ptr之前,它已经被淘汰了。

Item 9 如何删除容器中的元素

使用容器自带的成员函数remove或者erase

使用算法模板

// 删除指定值
// for vector, string or deque
c.erase(remove(c.begin(), c.end(), 1963), c.end());
// for list
c.remove(1963);
// for associate container
c.erase(1963);

// 删除满足条件的值
// for vector, string or deque
bool badValue(int x);
c.erase(remove_if(c.begin(), c.end(), badValue), c.end());
// for list
c.remove_if(badValue);
// for associate container
AssocContainer<int> c;
AssocContainer<int> goodValues;
remove_copy_if(c.begin(), c.end(), inserter(goodValues, goodValues.end()), badValue);
c.swap(goodValues);

关联容器不能使用算法模板removeremove_if,因为removeremove_if两个模板并没有真正的从容器中删除元素,只是将要删除的元素放在了容器的末尾。但是关联容器里的元素是按照key排序的,所以这种做法会损坏关联容器的内部结构。从关联容器中删除元素只能调用成员函数erase或者算法模板remove_copy_if把需要留下的元素拷贝到一个新的容器里,再进行swap。

我们经常通过迭代器来遍历容器中的元素,并在迭代过程中删除其中某些元素,一般有两种写法来实现这个操作:

  1. it = c.erase(it)
  2. c.erase(it++)

写法2有个问题,那就是对于sequence container,删除一个元素会导致该元素后面的所有元素的迭代器失效,所以it++操作产生的指向下一个元素的迭代器在it指向的这个元素被删除后就失效了。为了避免自己不经意踩到这个坑,那就无脑选择写法1吧。

Item 10 自定义的allocator

allocator最好实现为类模板

allocator需要提供两个typedef allocator<T>::pointerallocator<T>::reference

同一个类型的allocator只能保存static的数据,这样由不同allocator对象分配出去的内存才可以由任意一个allocator对象回收

allocator的接口是和new不一样的:

void* operator new(size_t bytes);
pointer allocator<T>::allocate(size_type numObjects);

虽然allocate的返回值是T*,但是返回的这块内存里可能并没有去初始化这些对象。

allocator内部需要提供一个rebind的模板,因为STL里的容器基本不会调用传入的allocator<T>来申请内存,而是调用allocator<U>来申请内存,U表示容器真正需要的数据结构类型,比如对于list,U就是节点。

template<typename T>
class allocator {
public:
	
  template<typename U>
  struct rebind {
    typedef allocator<U>other;
  };
};

这个rebind存在的原因是stl并不知道存入的allocator的模板名,但是stl又想要用相同的内存分配策略去分配去其他类型的数据结构,于是stl就可以使用这个rebind模板来引用传入的allocator的模板。

allocator的示例实现

  1. http://www.josuttis.com/cppcode/allocator.html
  2. https://blog.csdn.net/arduousbonze/article/details/3624177

Item 11 自定义allocator的适用场景

自己有更高效的内存管理算法;

对allocator的线程安全性没有要求,而默认的allocator有线程安全方面的开销;

使用自定义allocator从共享内存中申请空间;

void *pVecotrMemory = mallocShared(sizeof(SharedDoubleVec));
SharedDoubleVec *pv = new(pVectorMemory)SharedDoubleVec;

...
pv->~SharedDoubleVec();
freeShared(pVectorMemory);

容器的模板参数里指定的allocator只是用来在容器内部动态申请内存的,容器本身的一些成员变量并不是用这个allocator申请的,因此需要在一开始创建这个容器的时候,先按照上面的模式先申请共享内存,然后用placement new让容器在申请下来的这块共享内存上执行构造函数,这样创建的容器才是放在共享内存里的。在释放容器占用的共享内存时,也需要手动调用容器的析构函数。

使用自定义allocator从不同的堆中申请空间;

Item 12 STL的容器不保证线程安全

Item 13 尽量使用vector和string而不是使用C里面的数组

使用string时,注意string的实现使用了引用计数,在并行环境下可能这种性能优化反而适得其反,需要查阅官方文档看是否能够关闭引用计数这种feature。

Item 14 使用reserve函数来提前设置容量,避免容器扩张的过程中的reallocation带来的性能损耗

Item 15 介绍了四种string实现的内存布局

Item 16 把string和vector里的数据传给C风格的接口

Item 17 使用swap来消除capacity占据的多余空间

其实就是通过range constructor创建一个新的容器,然后swap。

Item 18 避免使用vector

vector<bool>并不是用vector来存的bool,vector<bool>也并不是容器,vector<bool>通过[]取出来的对象不是bool,而是一个代理,该代理表现的像bool。最直接的,该下面的代码通不过编译:

vector<bool> v;
bool *pb = &v[0];

可以考虑使用deque<bool>和bitset代替;

懂位运算的同学按自己的需求实现一个数据结构就好了;

Item 19 Equality and Equivalence

算法模板find使用的是equality,即用operator ==来进行比较,而set使用的是equivalence,用的operator <来进行比较,equivalence等价于!(w1<w2)&&!(w2<w1)。关联容器使用的都是equivalence。
往关联容器里插入元素,是用的equivalence判断里面是否已经有这个元素了,而不是使用的equality。关联容器需要一个less函数来做顺序的比较,但是有了这个less函数就不再需要equality了,因为如果引入equality,那么在findinsert的时候就应该使用equality来进行判断,比如对于set,可能导致插入两个equality上不等,但是equivalence上是相等的元素,这两个元素在set里的顺序是不定的,这使得set里的元素不再具有固定的顺序。

注意:关联容器的成员函数也是使用的equivalence,而算法模板使用的equality。

Item 20 记得可以通过指定关联容器的比较函数来自定义元素的排序规则

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

Compare必须是个类型,因此这里不能传函数指针了。

Item 21 关联容器的比较函数必须是严格的大于或小于

关联容器的比较函数对于相同(equality)的两个值必须返回false,否则按照equivalence两个equality意义上相等的元素就会被判定为不想等,这样set这种容器里就会含有相同的元素。
而对于multiset,multimap这种容器来说,如果传入的比较函数不是严格意义上的大于、小于,那么由于equal_range函数也是按照equivalence进行查找的,这也会导致multiset/multimap里equality意义上相等的元素不会返回在同一个equal_range的结果里。

Item 22 不要修改set和multiset里的key

map和multimap的模板类型里的key其实是const key类型,但是set、multiset模板的类型参数是没有const修饰符的,所以set、multiset里的元素可能是可以改变的,这个要看具体的stl库的实现是否允许你改变。比如我测试的g++ 9.3是不允许改变这个set,multiset里的元素的,因为这个版本的set、multiset的iterator是一个const的iterator,不允许你去改变。由于set、multiset里的元素是按less函数进行比较的,如果我们修改元素的其他部分而不修改用于less函数比较的部分,那么其实也是ok的,我们参考下面的代码即可,通过强制转换转为引用:

if (i != se.end()) {
	const_cast<Employee&>(*i).setTitle("Coporate Deity");
}

注意一定要强制转为引用,而不是强制转为对应的Employee类型,因为那样会为你生成一个临时变量,没有达到修改set、multiset中元素的效果。

Item 23 如果只是查询有序的数组可能比关联容器更高效

因为数组不用存节点指针,需要占用的内存更少,内存局部性更好,可能造成的page fault更少。

Item 24 map的三个函数[]、insert和emplace

书里面的内容有些过时,没有提及emplace函数,但是Stackoverflow上面已经有了充分的讨论。

首先是功能上的差别:

[]要求value是有默认构造函数的

[]可以更新key对应的value,而insertemplace不可以

其次是性能的差别:

在进行插入操作时,按书中所述[]等价于以下操作:

typedef map<int, Widget> IntWidgetMap;
pair<IntWidgetMap::iterator, bool> result = m.insert(IntWidgetMap::value_type(1, Widget()));
result.first->second = 1.50;

显然[]insert函数多了一个value的赋值操作;但在c++11里,这样其实也不高效,要用emplace搞完美转发才是最高效的。

在进行更新操作时,可以使用[]直接对value赋值更新,也可以使用insertemplace返回的iterator来更新,但是由于insertemplace可能产生一个临时对象(即pair,按照cppreference的说法,The element may be constructed even if there already is an element with the key in the container, in which case the newly constructed element will be destroyed immediately.),所以这时候是[]操作更高效。

所以书中就提出了一个总是最高效的effecientAddOrUpdate函数:

template <typename MapType, typename KeyArgType, typename ValueArgType>
MapType::iterator efficientAddOrUpdate(MapType& m, const KeyArgType& k, const ValueArgType& v)
{
	MapType::iterator lb = m.lower_bound(k);
	if (lb != m.end() && !(m.key_comp()(k, lb->first))) {
		lb->second = v;
		return lb;
	} else {
		m.insert(lb, {k, v});
	}
}

上述代码使用了c++11的template< class P > iterator insert( const_iterator hint, P&& value );这个函数。

另外需要注意的是Stackoverflow上举的insert的例子,std::map<K,V>::value_type实际上是std::pair<const K,V>,尤其注意这个const K,这导致下面的3比1、2的写法效率更低:

K t; V u;
std::map<K,V> m;           // std::map<K,V>::value_type is std::pair<const K,V>

m.insert( std::pair<const K,V>(t,u) );      // 1
m.insert( std::map<K,V>::value_type(t,u) ); // 2
m.insert( std::make_pair(t,u) );            // 3

因为3的写法生成的是pair<K,V>,在最终拷贝到map里的时候,会先转换成一个pair<const K, V>再拷贝到map的pair<const K, V>,多了中间这个转换的步骤。
因此3的写法需要改良成m.insert( std::make_pair<const K,V>(t,u) ); // 4,以避免这个类型推导带来的pair类型不匹配,需要转换的开销。

然而,使用emplace的话,又完全没有上面这个烦恼了,完美转发直接把参数传给std::pair<const Key, T>的构造函数,没有这个pair类型不对的问题。

Item 25 学会使用unordered_set与unordered_map

Item 26 优先使用iterator

四种迭代器:iteratorconst_iteratorreverse_iteratorconst_reverse_iterator
以及他们之间的转换关系:

  • 不带const的可以隐式转换为带const的;
  • reverse的可以通过base函数转换为普通的iterator
  • iterator可以隐式转换为reverse_iterator

书中举的支持使用iterator的例子:
当你拿iteratorconst_iterator进行比较(==)和相减的运算的时候,可能由于stl版本实现的问题,这个==-运算是实现为const_iterator的成员函数,如果你是iterator作为第一个操作数,而const_iterator作为第二个操作数,那么第一个操作数的iterator就不会自动转换为const_iterator导致编译报错。所以作者就建议我们干脆都使用iterator好了,免得遇到这些可能的麻烦。

Item 27 const_iterator转换为iterator

使用advancedistance函数:
advance(i, distance<Constler>(i, ci));

由于对于不能支持random access的容器来说,这种转换的代价不是O(1)的,所以作者又提醒我们不要给自己没事找事,用iterator就好。

Item 28 reverse_iterator的base函数

reverse_iteratorbase函数转出来的iterator并不是原来reverse_iterator所指的位置,巨坑。只能说用在插入操作时可以直接用转换出来的iterator,对于删除操作则需要先将原来的reverse_iterator进行++操作再转换为iterator再进行删除:

v.erase((++ri).base());

之所以不用

v.erase(--ri.base());

是因为编译器不允许函数返回的指针被修改(实际测试了下,报的错误是返回值是一个右值,不允许进行--操作)。

Item 30 Make sure destination ranges are big enough

就是当你使用transform算法模板将转换结果存放到另一个容器里的时候,要么使用合适的inserter将结果append或者insert到这个容器里去,要么提前resize,把结果以覆盖的方式写进去。

  • back_inserter
  • front_inserter
    要求容器有push_front函数
  • insert_iterator
  • inserter

Item 31 几种排序算法

  • sort
  • stable_sort
  • partial_sort
    这个算法可以用来选取前n个元素,并且只有这前n个元素保持有序,其余的元素不保证有序。
  • nth_element
    这个算法可以用来把容器按照某个分位划分为两半。

这几个都要求支持随机访问,因此这几个算法模板不能直接用在list上。

  • partition
  • stable_partition

则只要求bidirectional iterators。

Item 32 remove算法模板

remove算法模板会将给定range里不需要删除的元素移动到这个range的最前面,然后由于remove算法模板传入的参数只是iterator,无法获知传入的range所对应的容器,也就无法调用该容器的erase函数来进行真正的删除, 需要写代码的人在remove后面再手动调用该容器的erase函数:

vector<int> v;
...
v.erase(remove(v.begin(), v.end(), 99), v.end());
...

对于list来说,其成员函数remove则是可以真正删除元素的。

需要注意的是,调用remove后,remove返回的end和v.end()直间是需要被真正删除的元素,但是这些元素的值是可能被覆盖为其他的值的,不保证是我们指定要删除的值。

Item 33 这就导致了remove和remove_if不能用在保存指针的容器上面

因为经过removeremove_if操作后,要删除的元素的值可能已经被覆盖(overwrite)了,不能再根据元素里的指针去释放资源了。如果元素里保存的不是指针,而是智能指针,那么这种问题则不会出现,因为智能指针可以自动释放资源。

Item 34 注意某些算法模板是否要求输入有序

要求输入有序的算法模板:

  • binary_search
  • upper_bound
  • lower_bound
  • equal_range
  • set_union
  • set_intersection
  • set_difference
  • set_symmetric_difference
  • merge
  • inplace_merge
  • includes

不要求输入有序的算法模板:

  • unique
  • unique_copy

Item 35 实现case-insensitive的字符串比较

其实就是讲了两个算法模板:mismatch和lexicographical_compare的使用,以及not2的使用。

Item 36 实现copy_if

算法模板库里有的关于copy的算法:

  • copy
  • replace_copy
  • replace_copy_if
  • remove_copy
  • remove_copy_if
  • uninitialized_copy
  • copy_backward
  • reverse_copy
  • unique_copy
  • rotate_copy
  • partial_sort_copy

可是没有copy_if,因此需要自己实现:

template <typename InputIterator, typename OutputIterator, typename Predicate>
OutputIterator copy_if(InputIterator begin, InputIterator end, OutputIterator destBegin, Predicate p)
{
	while (begin != end) {
		if (p(*begin))
			*destBegin++ = *begin;
		++begin;
	}
	return destBegin;
}

因为not1是不能直接作用于一个函数指针/函数对象的,因此想按照下面这种方法写一个简化版本是行不通的:

template <typename InputIterator, typename OutputIterator, typename Predicate>
OutputIterator copy_if(InputIterator begin, InputIterator end, OutputIterator destBegin, Predicate p)
{
	return remove_copy_if(begin, end, destBegin, not1(p));
}

上述代码需要修正,解决方法是使用ptr_fun

	return remove_copy_if(begin, end, destBegin, not1(std::ptr_fun(p)));

备注:
c++11已经提供了copy_if这个算法模板,其可能的一种实现正是本书所介绍的实现。

Item 37 accumulate与for_each

accumulatefor_each的区别:
accumulate的函数对象不可以对输入的数据产生副作用,而for_each可以。

其他算法模板:

  • count
  • count_if
  • min_element
  • max_element

Item 38 pass-by-value的函数对象

对于pass-by-value的函数对象,需要保证两点:

  1. 函数对象类应该尽量小,以减少拷贝带来的性能损害;
  2. 函数对象不要使用多态;

如果你想要在函数对象里保存很多状态之类的数据的话,为了让这个类尽量小,可以考虑再写一个类保存这些数据,然后在函数对象里保存指向这个类的指针。这种做法也带来一个问题,那就是要处理好这个指针。

Item 39 predicate应该是pure functions

概念:

  • predicate
    返回值是bool(或者是可以隐式转换为bool的其他类型)的函数
  • pure function
    函数的返回值只和输入的参数有关,输入的参数相同,那么返回值也一定相同(有点函数式编程的意味)
  • predicate class
    operator()成员函数是predicate的functor class

对于stl里用到predicate作为参数的地方,无论传入的是predicate class的对象,还是函数指针,对应的这个predicate函数都应该实现为pure function。不要试图在这个predicate函数里用static变量之类的方法在多次调用之间维护状态,因为stl会调用多少次predicate,是否会复制传入的predicate class对象,都是不可控的,可能会超出我们的预料从而导致错误。

Item 40 Make functor classes adaptable

STL里有4个function adapters(not1not2bind1stbind2nd),可以根据传入的函数对象生成具有新的语义的函数对象。但是这些function adapters对传入的函数对象对应的functor class有要求,需要这个functor class里有一些类型定义(其实就是typedef,比如argument_typefirst_argument_typesecond_argument_typeresult_type)。含有这些类型定义的functor class就称之为adaptable,而要让functor class含有这些类型定义,最好的办法就是继承自std::unary_functionstd::binary_function

std::unary_functionstd::binary_function也不是能够直接继承的,它们只是类模板,需要根据functor class里的operator()的参数和返回类型生成模板类,然后再用于functor class的继承。

如果functor class的operator()函数的参数类型或者返回值类型不是指针类型,那么用于继承的std::unary_functionstd::binary_function的模板参数会去掉const&属性,反之则不去掉。这个原理要看具体的not1not2bind1stbind2nd这些function adapters的实现,看里面是如何处理模板参数类型的。

Item 41 ptr_fun,mem_fun与mem_fun_ref

STL里的函数模板只会使用f(x)的方式来调用传入的函数对象或者函数指针,但是我们有时候想传入的是成员函数,希望按照x.f()或者p->f()的方式来调用这个函数,这个时候我们就需要使用mem_funmem_fun_ref来处理一下我们传入的成员函数,让STL的函数模板仍然可以直接用f(x)的方式调用传入的函数对象或者函数指针。

ptr_fun,按照我的理解是Item 40中,用于处理函数指针,使其可以被not1not2bind1stbind2nd这些function adapters接受。

Item 42 不要通过模板特化改写std::less

std::less默认调用operator<,不要尝试通过模板特化改写std::less的行为,让其去做其他的事情,因为这样会造成误解。正常人读代码都会以为setmultsetmapmultimap这些东西是按照operator<来比较顺序的,你重写了std::less的行为,读代码的人不一定会清醒的注意到这一点。

Item 43 使用现有的算法库而不是手写算法(loop)

相比于自己手写循环、算法,使用**里的函数的好处有:

  • 效率
    STL愿意做我们懒得做的优化;
    STL用的算法一般比我们自己能够想出来的好;
    在处理STL里的数据结构时,<algorithm>里的算法实现由于比我们一般程序员更清楚这些数据结构是如何实现的,因此可以用上一些和这些数据结构相关的更底层的优化;
  • 正确性
    自己写可能会犯很多迭代器相关的错
  • 可维护性
    <algorithm>里的函数名字都取的很准确,容易理解代码到底在做什么

总之,在有现成的<algorithm>算法模板可用的时候,并且这些算法模板可以简化我们的代码、提升效率、增加可读性,最好就用这些算法模板。

Item 44 使用同名的成员函数而不是算法模板

  • 同名的成员函数可能具有更低的算法复杂度
  • 同名的成员函数可以更好的利用容器内部的数据结构
  • mapmultimap这种容器,可以使用equivalence而不是equality,只比较key,而不用比较整个pair

Item 45 用于在容器中搜索的几个算法模板

  • count
  • find
  • binary_search
  • lower_bound
  • upper_bound
  • equal_range

注意区分容器里的元素是否有序、容器本身是否提供了相应的更高效的成员函数。

Item 46 尽量使用函数对象而不是函数指针作为算法模板的参数

如果你的函数对象实现了内联(inline),那么算法模板在展开的时候可以使用这个内联。但是如果你传入的参数是函数指针,那么就可能没法使用这个内联了,这样反而可能导致传函数指针的性能低于传函数对象的性能。

Item 47 不要写write-only code

  • 你可以写高密度(代码量少)、高性能(节约的性能可以忽略不计)来折磨你的队友,也可以选择做个好人;
  • 写的时候想的不够清楚,靠修修补补出来的代码对别人的可读性大概率不好,这就是所谓的write-only code;

Item 48 include头文件时要写正确

不要偷懒,用了什么STL的什么组件就要包含相应的头文件,虽然某些头文件可能已经帮你包含了另外的所需要的头文件,但是这种包含关系可能换了STL库之后就没有了,你的代码就有移植性问题了。
作者列了几条规则来帮助记忆所需头文件:

  • 容器的头文件和容器本身的名字基本相同;
  • 除了accumulateinner_productadjacent_differencepartial_sum是四个算法是声明在<numeric>中的, 其他算法都是声明在<algorithm>中的;
  • istream_iteratorsistreambuf_iterators这种特殊的迭代器声明在<iterator>里;
  • less<T>这种functor声明在<functional>中;

Item 49 如何阅读STL相关的编译报错信息

主要是讲了STL相关的报错信息可能会含有一大堆模板类,可以通过字符串替换把这些模板类的名字变简单短小一些来简化报错信息。
在const member function中非静态成员变量都会默认变成const的这个举例倒是令人印象深刻。

Item 50 STL相关网站

  • SGI STL
    http://www.sgi.com/tech/stl/
    貌似已经访问不了了,不过倒是网上发现了一个别人的SGI STL的笔记
    SGI STL已经成为GNU C++标准库的一部分了,所以直接看g++附带的STL源码好了。
  • STLport
    http://www.stlport.org/
    两个优点:
    • 有跨平台的优化,减少使用其他stl库可能遇到的移植上的坑;
    • 提供一种debug模式,可以检测出使用stl相关的错误(比如乱踩内存然后程序又不崩溃就需要这种方法来检测出来);
  • boost
    http://www.boost.org/
    在stl里面找不到的feature、功能有可能在boost库里就可以找到,比如我就是在boost库里找到了共享内存的相关组件。