前言
STL在C++开发中的重要性无需多言。它设计精妙,性能卓越,功能方便。
大多数时候,它都易于使用。但利刃能开几分光,还要依赖使用者的修养。提修养,多读书。《Efficient STL》中给出了关于STL的50条建议。
这些规则分为以下七个部分:
-
$\color{blue}{【Container(容器)】}$
-
$\color{blue}{【vector\&string(列表和字符串)】}$
-
$\color{blue}{【associate container(关联型容器)】}$
-
$\color{blue}{【Iterator(迭代器)】}$
-
$\color{blue}{【Algorithms(算法)】}$
-
$\color{blue}{【Function(函数)】}$
-
$\color{blue}{【编码实践】}$
本文将介绍Container部分的内容。希望它们提供快如闪电的性能,同时照亮使用过程中的疑惑。
容器篇
好好考虑要用什么样的容器。
尽量通过typedef或者using让代码不依赖于容器,方便后续的测试和替换:
class C {...};
std::vector<C> C_list; //Bad,如果不用std::vector,上下文需要更改许多代码
typedef std::vector<ABC> CContainer;
typedef C::iterator CCIterator; // 每个容器都会定义自己的iterator
CContainer C_list; //Good, 利用typedef的类型声明变量,这样后续替换容器类型的话,不需要到处更改代码
在insert或者push_back对象时,STL默认使用拷贝进行构造;所以如果容器内存储的是一个比较大的对象或者涉及父类、子类的操作,应该存储对象的$\textbf【指针】$ 而不是对象本身;
用empty()检查是否为空,不要用size()是否等于0来判断
有些容器的size()操作时间复杂度并不是O(1),比如List;
尽量用range function代替手动循环逐个操作。
std::vector<int> b{...};
std::vector<int> firstTenOfB;
firstTenOfB.assign(b.begin(), b.begin() + 10); // good
// awful
int cnt = 0;
for (auto &b_element: b) {
firstTenOfB.push_back(b_element);
cnt++;
if (cnt == 10) break;
}
逐个操作调用了多次insert,可能会带来额外的函数调用开销(inline也许可以减少这个损耗);在比如vector的操作中,如果向非结尾部分插入元素,insert多次还会造成元素的多次拷贝移动,assign则可以事先计算出偏移量,一次移动到位;
尽量避免圆括号的歧义:初始化用花括号,嵌套的带括号的函数参数单独声明;
//wrong example:
ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());
//定义了一个名为data的函数,接收两个类型为istream_iterator的参数
//C++优先解析为函数,dataFile将被视为参数,括号会被忽略
// correct
ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);
如果用容器存储裸指针,在容器析构前需要手动delete,否则会造成内存泄漏。
不要用容器存储auto_ptr, auto_ptr拷贝时会导致值改变
不同的容器有各自的元素删除方式:
//删除特定的值
// vector, string, deque
c.erase( remove(c.begin(), c.end(), 1963), c.end());
//list
c.remove(1963);
//关联容器: unordered_set, unordered_map, map, set, multimap, multiset
c.erase(1963);
//删除满足某一条件的值,并且进行一些额外的操作,比如打日志
ofstream File; // log file to write to
AssocContainer<int> c; //关联容器, set, map, hashset, hashmap等
for (AssocContainer<int>::iterator i =c.begin(); i != c.end(); ) {
if (badValue(*i)) {
File« "Erasing"« *i «'\n';
c.erase(i++);
// loop conditions are the
// same as before
} else ++i;
// keep i valid by assigning
} // erase's return value to it
else ++i;
SeqContainer<int> c; //vector, list
for (SeqContainer<int>::iterator i = c.beginO; i != c.endO; ) {
if (badValue(*i)) {
File« "Erasing"« *i« '\n';
i = c.erase(i); // erase会返回下一个可用的迭代器
}
}
关于allocator: 它可以定义自己的reference和pointer类型,不过STL会使用默认的T*和T&;同一个的类型的allocator应该提供等价的行为,所以自定义的allocator实例不该有nonstatic的成员;如果没有调用其他实例化操作,Allocator不会指向实例对象;Allocator需要提供rebind模板接口:Allocator::rebind[HTML_REMOVED]::other
allocator不能做:让局部变量通过allocator使用sharedMemory
虽然allocator会从sharedMemory获取数据,但v仍然是存储在局部堆栈空间里的;需要手动new内存,在指定内存上创建object;离开作用域时再手动析构;
allocator能做:通过指定自定义的堆类型,让对象分配到不同的内存堆中;
C++的STL对线程安全的支持非常有限;部分版本可以支持:1)多个reader同时读取是安全的;2)多个writer同时写入不同的container是安全的。
所以用户需要实现自己的锁机制。推荐使用RAII机制, C++中的lock_guard实现了这点:
#include <lock_guard>
{ // begin of critical area
std::lock_guard<std::mutex> guard(your_lock);
// do something
} // end of critical area, 锁在析构时会释放;