迅速读懂:Effective STL
Posted Hello_Motty
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了迅速读懂:Effective STL 相关的知识,希望对你有一定的参考价值。
如果有连续空闲的几天,其实可以找原书进行阅读,毕竟这里的是缩略的内容,和原版中的举例比起来这里的大部分内容只是对规则的总结。
但是要是说C++有什么特殊的科技,STL确实是其中之一,很多人评价STL源码写的像屎一样,又长又绕是真的,不过初衷可能是为了让大多数不去看源码人愉快的使用吧。
条款11:理解自定义分配器的正确用法
如果出于以下某种原因让你觉得你需要自己写一个allocator,比如:默认的在需求中太慢、浪费内存或造成过度的碎片, 或者你只对单线程的程序感兴趣, 或者想建立一个相当共享内存的唯一的堆并且把一个或多个容器放在那块内存里。都适合重新写一个自定义allocator。
1.如果想使用共享内存的堆,并把stl容器的内容放在共享内存中,代码如下:
1 //自定义堆分配内存 2 void* mallocShared(size_t bytesNeeded); 3 void freeShared(void *ptr); 4 //共享内存分配器模板 5 template<typename T> 6 class SharedMemoryANocator { 7 public: 8 ... 9 pointer allocate(size_type numObiects, const void *localityHint = 0) 10 { 11 return static_cast<pointer>(mallocShared(numObiects * sizeof(T))); 12 } 13 void deallocate(pointer ptrToMemory, size_ type numObjects) 14 { 15 freeShared(ptrToMiemory); 16 } 17 ... 18 };
使用的时候这样
1 typedef vector<double, SharedMemoryAllocator<double> > 2 SharedDoubleVec; 3 ... 4 { // 开始一个块 5 SharedDoubleVec v; // 建立一个元素在 6 // 共享内存中的vector 7 ... // 结束这个块 8 }
当然这样分配后v分配的容纳元素的内存在共享内存中,但v本身和v的数据成员仍然在堆中,如果要把v放在共享内存上,还需要进行另外的操作:
1 void *pVectorMemory = // 分配足够的共享内存 2 mallocShared(sizeof(SharedDoubleVec)); // 来容纳一个SharedDoubleVec对象 3 SharedDoubleVec *pv = // 使用“placement new”来 4 new (pVectorMemory) SharedDoubleVec; // 在那块内存中建立一个SharedDoubleVec对象; 5 6 // 这个对象的使用(通过pv) 7 ... 8 pv->~SharedDoubleVec(); // 销毁共享内存中的对象 9 freeShared(pVectorMemory); // 销毁原来的共享内存块
作为一个严谨的程序员还应该在后面加上“pVectorMemory = NULL;”来避免产生野指针。除非你真的要让一个容器(与它的元素相反)在共享内存里,否则我希望你能避免这个手工的四步分配/建造/销毁/回收的过程。
2.假设你有两个堆,命名为Heap1和Heap2类。每个堆类有用于进行分配和回收的静态成员函数:
1 class Heap1 { 2 public: 3 ... 4 static void* alloc(size_t numBytes, const void *memoryBlockToBeNear); 5 static void dealloc(void *ptr); 6 ... 7 }; 8 class Heap2 { ... }; // 有相同的alloc/dealloc接口
你想在不同的堆里联合定位一些STL容器的内容,使用像Heap1和Heap2那样用于真实内存管理的类:
1 template<typenameT, typename Heap> 2 class SpecificHeapAllocator { 3 public: 4 pointer allocate(size_type numObjects, const void *localityHint = 0) 5 { 6 return static_cast<pointer>(Heap::alloc(numObjects * sizeof(T), localityHint)); 7 } 8 void deallocate(pointer ptrToMemory, size_type numObjects) 9 { 10 Heap::dealloc(ptrToMemory); 11 } 12 ... 13 };
然后你使用SpecificHeapAllocator来把容器的元素集合在一起:
1 vector<int, SpecificHeapAllocator<int, Heap1 > > v; // 把v和s的元素 2 set<int, SpecificHeapAllocator<int Heap1 > > s; // 放进Heap1 3 list<Widget, SpecificHeapAllocator<Widget, Heap2> > L; // 把L和m的元素放进Heap2 4 map<int, string, less<int>, SpecificHeapAllocator<pair<const int, string>,Heap2> > m;
很重要的一点是Heap1和Heap2是类型而不是对象,STL为用不同的分配器对象初始化相同类型的不同STL容器提供了语法(原书没写具体是什么,不过我猜STL源码剖析上面一定有),这里如果Heap1和Heap2是对象,那么STL分配器将是不等价的,从而违背了其等价分配约束。
这两个例子中演示的分配器大部分情况下都是available的,只要遵循其约束就可以自定义分配器满足项目要求。
条款12:对STL容器线程安全性的期待现实一些
大部分厂商对多线程支持的黄金定律:
- 多个读取者是安全的。多线程可能同时读取一个容器的内容,这将正确地执行。当然,在读取时不能有任何写入者操作这个容器。
- 对不同容器的多个写入者是安全的。多线程可以同时写不同的容器。
举个例子:
1 //它搜寻一个vector<int>中第一次出现5这个值的地方,而且,如果它找到了,就把这个值改为0。 2 vector<int> v; 3 vector<int>::iterator first5(find(v.begin(), v.end(), 5)); // 行1 4 if (first5 != v.end()){ // 行2 5 *first5 = 0; // 行3 6 }
在单线程环境下无疑是安全的、可实现的;在多线程环境下:另一个线程可能在行1完成之后立刻修改v中的数据,那行2行3无意义;行3中对*first5的赋值是不安全的,因为另一个线程可能在行2和行3之间执行,并以某种方式使first5失效。
要让上面的代码成为线程安全的,v必须从行1到行3保持锁定,STL自身并不能推断出这个,使用原语开销很大,但是也基本无法想象出没有性能损失情况下解决这个问题。所以必须人工控制这个过程,添加互斥量模板类:
1 template<typename Container> // 获取和释放容器的互斥量 2 class Lock { // 的类的模板核心; 3 public: // 忽略了很多细节 4 Lock(const Containers container) 5 : c(container) 6 { 7 getMutexFor(c); // 在构造函数获取互斥量 8 } 9 ~Lock() 10 { 11 releaseMutexFor(c); // 在析构函数里释放它 12 } 13 private: 14 const Container& c; 15 };
使用时
1 vector<int> v; 2 ... 3 { // 建立新块; 4 Lock<vector<int> > lock(v); // 获取互斥量 5 vector<int>::iterator first5(find(v.begin(), v.end(), 5)); 6 if (first5 != v.end()) { 7 *first5 = 0; 8 } 9 }
基于Lock的方法在有异常的情况下是稳健的。C++保证如果抛出了异常,局部对象就会被销毁,所以即使当我们正在使用Lock对象时有异常抛出,Lock也将释放它的互斥量。
当涉及到线程安全和STL容器时,你可以确定库实现允许在一个容器上的多读取者和不同容器上的多写入者。你不能希望库消除对手工并行控制的需要,而且你完全不能依赖于任何线程支持。
条款13:尽量使用vector和string来代替动态分配的数组
使用new来进行动态分配,你需要肩负下列职责:必须确保有的人以后会delete这个分配→必须确保使用了delete的正确形式→必须确保只delete一次。所以大多数情况下,可以使用vector或者string分配动态数组。
有一种特殊情况只关系到string的使用:很多string实现在后台使用了引用计数,在多线程环境中使用了引用计数的字符串,你可能发现避免分配和拷贝所节省下的时间都花费在后台并发控制上,不过也不是全无办法的,或者至少不会让你放弃使用stl:1. 看看你的库实现是否可以关闭引用计数,通常是通过改变预处理变量的值。当然那是不可移植的,但使工作变得可能;2. 寻找或开发一个不使用引用计数的string实现(或部分实现)替代品(其实可以参考非引用计数型string实现); 3.考虑使用vector<char>来代替string,不过要改变string相关的函数。所有的结果都是简单的。如果你在使用动态分配数组,你可能比需要的做更多的工作。
条款14:使用reserve来避免不必要的重新分配
stl容器只要不超过他们的大小,他们都可以自动增长以满足你放进去的元素。对于vector和string,只要需要更多空间,就以realloc等价的思想来增长。但是realloc并不是完美的,每次执行都要的分配,回收,拷贝和析构,而执行这些步骤所有指向vector或string中的迭代器、指针和引用都会失效,这意味着简单地把一个元素插入vector或string的动作也可能因为需要更新其他使用了指向vector或string中的迭代器、指针或引用的数据结构而膨胀。通常有两情况使用reserve来避免不必要的重新分配。第一个可用的情况是当你确切或者大约知道有多少元素将最后出现在容器中。第二种情况是保留你可能需要的最大的空间,然后,一旦你添加完全部数据,修整掉任何多余的容量。
条款15:小心string实现的多样性
每个string实现都容纳了下面的信息:字符串的大小,也就是它包含的字符的数目;容纳字符串字符的内存容量;这个字符串的值,也就是,构成这个字符串的字符。有一些string可能容纳它的配置器的拷贝、这个值的引用计数。
string的4种实现:
- 每个string对象包含一个它配置器的拷贝,字符串的大小,它的容量,和一个指向包含引用计数(“RefCnt”)和字符串值的动态分配的缓冲区的指针。在这实现中,一个使用默认配置器的字符串对象是指针大小的四倍。对于一个自定义的配置器,string对象会随配置器对象的增大而变大
- string对象和指针一样大,因为在结构体中只包含一个指针,string指向的对象包含字符串的大小、容量和引用计数,以及容纳字符串值的动态分配缓冲区的指针。对象也包含在多线程系统中与并发控制有关的一些附加数据
- string对象总是等于指针的大小,但是这个指针指向一个包含所有与string相关的东西的动态分配缓冲器:它的大小、容量、引用计数和值。没有每物体配置器(per-object allocator)的支持。缓冲区也容纳一些关于值可共享性的数据
- string对象是一个指针大小的七倍(仍然假设使用了默认配置器)。这个实现没有使用引用计数,但每个string包含了一个足以表现最多15个字符的字符串值的内部缓冲区。因此小的字符串可以被整个保存在string对象中,一个有时被称为“小字符串优化”的特性。当一个string的容量超过15时,缓冲器的第一部分被用作指向动态分配内存的一个指针,而字符串的值存放在那块内存中
- string对象的大小可能从1到至少7倍char*指针的大小。
- 新字符串值的建立可能需要0、1或2次动态分配。
- string对象可能是或可能不共享字符串的大小和容量信息。
- string可能是或可能不支持每对象配置器。
- 不同实现对于最小化字符缓冲区的配置器有不同策略。
条款16:如何将vector和string的数据传给遗留的API
如果你有一个vector对象v,而你需要得到一个指向v中数据的指针,以使得它可以被当作一个数组,只要使用&v[0]就可以了。对于string对象s,相应的咒语是简单的s.c_str()。但是必然有一些限制
1 void doSomething(const int* pInts, size_t numInts); 2 //如果v是空的。v.size()是0,而&v[0]试图产生一个指向根本就不存在的东西的指针 3 if (!v.empty()) { 4 doSomething(&v[0], v.size()); 5 }
如果你基于某些原因决定键入v.begin(),就应该键入&*v.begin(),因为这将会产生和&v[0]相同的指针,而且让你拥有练习打字的机会。
但是string不可以这样取得,因为string既不是连续分配的内存也没有‘\0’结束符,所以要按照模板内函数的形式:(如果string中内容包含‘\0’,转化后有可能出错)
1 void doSomething(const char *pString); 2 doSomething(s.c_str());
如果你想用C风格API返回的元素初始化一个vector,你可以利用vector和数组潜在的内存分布兼容性将存储vecotr的元素的空间传给API函数:
1 // C API:此函数需要一个指向数组的指针,数组最多有arraySize个double 2 // 而且会对数组写入数据。它返回写入的double数,不会大于arraySize 3 size_t fillArray(double *pArray, size_t arraySize); 4 vector<double> vd(maxNumDoubles); // 建立一个vector,它的大小是maxNumDoubles 5 vd.resize(fillArray(&vd[0], vd.size())); // 让fillArray把数据写入vd,然后调整vd的大小为fillArray写入的元素个数
C风格API把数据放入一个vector,然后拷到你实际想要的STL容器中的主意总是有效的:
1 // C API:此函数需要一个指向数组的指针,数组最多有arraySize个char 2 // 而且会对数组写入数据。它返回写入的char数,不会大于arraySize 3 size_t fillArray(double *pArray, size_t arraySize); 4 vector<double> vd(maxNumDoubles); // 建立一个vector,它的大小是maxNumChars 5 vd.resize(fillArray(&vd[0], vd.size()));//让fillString把数据写入vd 6 deque<double> d(vd.begin(), vd.end()); // 拷贝数据到deque 7 list<double> l(vd.begin(), vd.end()); // 拷贝数据到list 8 set<double> s(vd.begin(), vd.end());// 拷贝数据到set
这也提示了vector和string以外的STL容器如何将它们的数据传给C风格API。只要将容器的每个数据拷到vector,然后将它们传给API:
1 void doSomething(const int* pints, size_t numInts); // C API (同上) 2 set<int> intSet; // 保存要传递给API数据的set 3 ... 4 vector<int> v(intSet.begin(), intSet.end()); // 拷贝set数据到vector 5 if (!v.empty()) doSomething(&v[0], v.size()); // 传递数据到API
条款17:使用“交换技巧”来修整过剩容量(仅适用于vector和string)
先举个例子,先创建个容器,然后获得了很多元素。
1 class Contestant {...}; 2 vector<Contestant> contestants;
但是很快发现大部分元素是没有用的,那就把元素删了,但是空间仍然还是那么大,怎么办呢?当然可以通过resize方法去改变,但是如果我不知道大概要多少,我只知道有一些符合我的要求,如果直接用resize可能大了很多,或者抛弃了原本符合要求的元素。所以最好有一种shrink_to_fit的方式。
1 vector<Contestant>(contestants).swap(contestants);
表达式vector<Contestant>(contestants)建立一个临时vector,它是contestants的一份拷贝:vector的拷贝构造函数做了这个工作。但是,vector的拷贝构造函数只分配拷贝的元素需要的内存,所以这个临时vector没有多余的容量。然后我们让临时vector和contestants交换数据,这时我们完成了,contestants只有临时变量的修整过的容量,而这个临时变量则持有了曾经在contestants中的发胀的容量。在这里(这个语句结尾),临时vector被销毁,因此释放了以前contestants使用的内存。同样的技巧可以应用于string。交换技巧的变体可以用于清除容器和减少它的容量到你的实现提供的最小值。
条款18:避免使用vector<bool>
做为一个STL容器,vector<bool>确实只有两个问题。第一,它不是一个STL容器。第二,它并不容纳bool。如果c是一个T类型对象的容器,且c支持operator[],如果你使用operator[]来得到Container<T>中的一个T对象,你可以通过取它的地址而获得指向那个对象的指针(假设T没有倔强地重载一些操作符)。
1 vector<bool> v; 2 bool *pb = &v[0]; // 用vector<bool>::operator[]返回的东西的地址初始化一个bool*
但是上面的这段代码不能通过编译。在一个典型的实现中,每个保存在“vector”中的“bool”占用一个单独的比特,而一个8比特的字节将容纳8个“bool”。在内部,vector<bool>使用了与位域(bitfield)等价的思想来表示它假装容纳的bool。你可以创建指向真的bool的指针,但却禁止有指向单个比特的指针。
为了解决这个难题,vector<boo>::operator[]返回一个对象,其行为类似于比特的引用,也称为代理对象。
1 template <typename Allocator> 2 vector<bool, Allocator> { 3 public: 4 class reference {...}; // 用于产生引用独立比特的代理类 5 reference operator[](size_type n); // operator[]返回一个代理 6 ... 7 };
所以,vector<bool>::reference*类型,不是bool*。
那当我需要一个vector<bool>时应该用什么?1.deque<bool> 但是不能传递deque<bool>中的数据给一个希望得到bool数组的C API,2. bitset 它不是容器,但是是stl标准库的一部分,它不支持插入和删除元素也不支持iterator。
现实地说:vector<bool>不满足STL容器的必要条件,你最好不要使用它。
条款19:了解相等和等价的区别
STL充满了比较对象是否有同样的值。find算法和set的insert成员函数是很多必须判断两个值是否相同的函数的代表。find对“相同”的定义是相等,基于operator==。set::insert对“相同”的定义是等价,通常基于operator<。
相等的概念是基于operator==的。如果表达式“x == y”返回true,x和y有相等的值,否则它们没有。这很直截了当,但要牢牢记住,因为x和y有相等的值并不意味着所有它们的成员有相等的值。等价是基于在一个有序区间中对象值的相对位置,两个对象x和y如果在关联容器c的排序顺序中没有哪个排在另一个之前,那么它们关于c使用的排序顺序有等价的值。
标准关联容器保持有序,所以每个容器必须有一个定义了怎么保持东西有序的比较函数(默认是less)。等价是根据这个比较函数定义的,所以标准关联容器的用户只需要为他们要使用的任意容器指定一个比较函数。如果关联容器使用相等来决定两个对象是否有相同的值,那么每个关联容器就需要,除了它用于排序的比较函数,还需要一个用于判断两个值是否相等的比较函数。通过只使用一个比较函数并使用等价作为两个值“相等”的意义的仲裁者,标准关联容器避开了很多会由允许两个比较函数而引发的困难。它避免了会由在标准关联容器中混用相等和等价造成的混乱。
条款20:为指针的关联容器指定比较类型
举个例子:
1 set<string*> ssp; // ssp = “set of string ptrs” 2 ssp.insert(new string("Anteater")); 3 ssp.insert(new string("Wombat")); 4 ssp.insert(new string("Lemur")); 5 ssp.insert(new string("Penguin")); 6 7 for (set<string*>::const_iterator i = ssp.begin(); // 你希望看到这个:“Anteater”,“Lemur”,“Penguin”,“Wombat” 8 i != ssp.end(); 9 ++i) 10 cout << *i << endl;
但是实际打印的是四个十六进制的数,*i不是一个string,它是一个string的指针。所以可能把for循环变成了某种非循环形式。比如:
1 copy(ssp.begin(), ssp.end(), ostream_iterator<string>(cout,"\n"));
然而很不幸编译出错,因为这个copy的调用将不能编译,ostream_iterator需要知道被打印的对象的类型,所以当你告诉它是一个string时(通过作为模板参数传递),编译器检测到那和ssp中储存的对象类型(是string*)之间不匹配。然后你又该回了原来的for循环,把*i变成了**i,但仍然只有1/24的可能性是按顺序输出的。所以应该从头审视这个set到底应该如何定义,set的完整定义如下:想要按顺序输出,我们就必须要让仿函数类less<string*>符合我们的要求:
1 set<string*, less<string*>, allocator<string*> > ssp;
所以我们要比较元素解引用的内容,就不能使用默认类型,而是要自定义我们的比较类型
1 struct StringPtrLess: 2 public binary_function<const string*const string*bool> { 3 bool operator()(const string *ps1, const string *ps2) const 4 { 5 return *ps1 < *ps2; 6 } 7 };
然后你可以使用StringPtrLess作为ssp的比较类型
1 typedef set<string*, StringPtrLess> StringPtrSet; 2 StringPtrSet ssp;// 建立字符串的集合,按照StringPtrLess定义的顺序排序,和前面一样插入
然后循环输出就可以得到一开始想要的结果,这种自定义比较类型的方式同样适用于智能指针和迭代器。
TBC
以上是关于迅速读懂:Effective STL 的主要内容,如果未能解决你的问题,请参考以下文章