写这种文章估计又要被一些"砖家"拍砖了,但是今天突然想反思一下这段时间剖析的STL源码,于是就有了本文.纯属个人观点,希望"砖家"们手下留情。
什么是STL(这里)?
STL = Standard Template Library,标准模板库,是现代C++程序设计的重要组成部分,同时也是C++标准的一部分,对应编程范式的generic programming(泛型编程)。其核心思想是数据与算法的分离,这就催生了algorithm(算法)和container(容器),而将二者联系在一起的工具就是充当粘合剂的iterator(迭代器),这验证了计算机世界中的一句名言“任何问题都可以通过增加一个间接层来解决”:-)。
STL的组建都是高度可复用的,也是高度可配置、可扩充的,我们完全可以根据需要进行相应的优化。在C++标准中,STL被组织为下面的13个头文件:<algorithm>、<deque>、<functional>、<iterator>、<vector>、<list>、<map>、<memory>、<numeric>、<queue>、<set>、<stack>和<utility>。其中由于历史原因,非常重要的数据结构hash_table没有被加入进去,这不得不能说是一种遗憾,不过Boost库解决了这个问题(BTW,Boost库中的每一项特性都可以看作是对C++语言缺陷的一种弥补:P),其实在常见的STL实现中,比如我正在剖析的SGI STL(这里)就提供了<hash_table>的实现,另外还有<slist>、<hash_map>、<hash_set>等非标准容器。
STL优点
既然要说STL欠缺什么,那么我们就先来看看STL都能干什么,有什么优势。
首先,STL封装了常用数据结构,避免了程序员重复发明相同功能的轮子(其实很多语言都直接提供对基本数据结构的支持,例如Java)。
其次,STL是高度可配置、可复用、可扩展的一系列组件,其算法效率非常高,很大程度上是等价甚至超越大部分程序员手写的代码(大家看看SGI STL的<stl_alloc.h>,其内存池设计的极其巧妙,堪称艺术品)。
再次,STL提供了优秀的可移植能力,在不同平台上都可以使用。
最后,STL能极大程度上提升程序员的编程效率。
下面给出一个非常简单的例子,让大家体验一下STL的强大:
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <iterator>
using namespace std;
int main()
{
vector<int> vec;
int input = 0;
// 这里使用cin,eof作结束, 按键视具提平台
// Windows --> Ctrl + Z
// Linux --> Ctrl + D
while (cin >> input)
{
// 这里可以动态调整容器大小, 体现了STL的灵活
vec.push_back(input);
}
// STL默认排序使用的是升序排列
sort(vec.begin(), vec.end());
// 输出所有元素, 并用空格分隔
copy(vec.begin(), vec.end(),
ostream_iterator<int>(cout, " "));
return 0;
}
想一想,上面的这个程序你要是完全手写要多长时间?要调试多久?我只用了五分钟就完成了这个STL版本的程序,是不是很强大?
STL不足
既然写了一个具体例子,那么我们就说说上面例子的缺点:
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <iterator>
using namespace std;
int main()
{
// 这里使用的是vector的默认构造函数, 根据实现的不同, 可能会预留内存空间,
// 也可能不预留, 比如我剖析的SGI STL就不会预留内存
vector<int> vec;
int input = 0;
while (cin >> input)
{
// 如果数据量很大的时候, 会导致频繁分配内存, 这带来的开销非常大
// 解决方案是调用reserve()进行内存的预留, 但是这对程序员的要求
// 比较高, 要求其对STL比较熟悉, 才能避开一些晦涩的问题
vec.push_back(input);
}
// STL默认排序使用的是升序排列
sort(vec.begin(), vec.end());
// 这里如果使用wcout就会造成不兼容的情况, 解决方案见后续文章
copy(vec.begin(), vec.end(),
ostream_iterator<int>(wcout, L" "));
return 0;
}
看看上面这段程序在Fedora 15 + CodeBlocks + G++环境下给出的错误:/home/mdl/SourceCode/BoostTest/main.cpp|29|错误:对‘std::ostream_iterator<int>::ostream_iterator(std::wostream&, const wchar_t [2])’的调用没有匹配的函数|
/home/mdl/SourceCode/BoostTest/main.cpp|29|附注:备选是:|
/usr/lib/gcc/i686-redhat-linux/4.6.0/../../../../include/c++/4.6.0/bits/stream_iterator.h|187|附注:std::ostream_iterator<_Tp, _CharT, _Traits>::ostream_iterator(const std::ostream_iterator<_Tp, _CharT, _Traits>&) [with _Tp = int, _CharT = char, _Traits = std::char_traits<char>, std::ostream_iterator<_Tp, _CharT, _Traits> = std::ostream_iterator<int>]|
/usr/lib/gcc/i686-redhat-linux/4.6.0/../../../../include/c++/4.6.0/bits/stream_iterator.h|187|附注: 备选需要 1 实参,但提供了 2 个|
/usr/lib/gcc/i686-redhat-linux/4.6.0/../../../../include/c++/4.6.0/bits/stream_iterator.h|183|附注:std::ostream_iterator<_Tp, _CharT, _Traits>::ostream_iterator(std::ostream_iterator<_Tp, _CharT, _Traits>::ostream_type&, const _CharT*) [with _Tp = int, _CharT = char, _Traits = std::char_traits<char>, std::ostream_iterator<_Tp, _CharT, _Traits>::ostream_type = std::basic_ostream<char>]|
/usr/lib/gcc/i686-redhat-linux/4.6.0/../../../../include/c++/4.6.0/bits/stream_iterator.h|183|附注: no known conversion for argument 1 from ‘std::wostream’ to ‘std::ostream_iterator<int>::ostream_type&’|
/usr/lib/gcc/i686-redhat-linux/4.6.0/../../../../include/c++/4.6.0/bits/stream_iterator.h|171|附注:std::ostream_iterator<_Tp, _CharT, _Traits>::ostream_iterator(std::ostream_iterator<_Tp, _CharT, _Traits>::ostream_type&) [with _Tp = int, _CharT = char, _Traits = std::char_traits<char>, std::ostream_iterator<_Tp, _CharT, _Traits>::ostream_type = std::basic_ostream<char>]|
/usr/lib/gcc/i686-redhat-linux/4.6.0/../../../../include/c++/4.6.0/bits/stream_iterator.h|171|附注: 备选需要 1 实参,但提供了 2 个|
||=== Build finished: 8 errors, 0 warnings ===|
不知道面对这样一堆提示你做何感想,这是STL暴露出来的第一个缺点,一旦出错,其提示信息会非常晦涩难懂。
当然这仅仅是STL在表面上能看到的不足,那么其在更深层次有什么不足呢?
首先,STL在设计的时候并没有考虑到线程安全,他们认为这是程序员的责任。好吧。。。我承认,但是试问有多少人能处理好这个问题?为什么在语言层次不提供支持?不过好消息是C++0已经能在语言层次支持线程同步了。同样是在我剖析的SGI STL<stl_alloc.h>中,其对线程安全支持也仅仅是allocator层次的,算法和容器并不能享受这样的待遇。
其次,STL的教学一直得不到应有的重视,试问有多少大学在教授C++的时候教授STL?
再次,如果对STL了解不足,那么可能带来非常低的效率。见下面代码:
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <iterator>
using namespace std;
class ExpensiveCopyObject
{
public:
ExpensiveCopyObject(string hugeData =
string("This is a very long string...............................")) :
hugeData_(hugeData)
{
cout << "Construct" << endl;
}
~ExpensiveCopyObject()
{
cout << "Destruct" << endl;
}
ExpensiveCopyObject(const ExpensiveCopyObject &other) :
hugeData_(other.hugeData_)
{
cout << "Copy" << endl;
}
ExpensiveCopyObject &operator =(ExpensiveCopyObject &rhs)
{
cout << "ioperator =()" << endl;
this->hugeData_ = rhs.hugeData_;
return *this;
}
// ......
private:
string hugeData_;
};
using namespace std;
int main()
{
// 想想一下这个vector要是内存重新非陪开销有多大?
// 比较好的作法是在vector存储指针, 但是这需要程序员自己控制指针生命周期
// 这样做很容易导致野指针问题, 所以不推荐
// 最好的解决方案是使用::boost::shared_ptr
// 具体详情见后续文章
vector<ExpensiveCopyObject> vec(10);
// ........
return 0;
}
最后,STL的学习曲线比较陡峭,其设计理念也需要长时间在实践中反思才能领悟,本来C++语法已经让很多人头疼不已,更不用说那些虚函数表一类的东西了,至于调试模板时的一次次打击,让很多人选择了离开,转向了Java、C#等更简单的语言。即使坚持了过来,又有多少人肯花时间在STL源码中探寻宝藏?这都成了STL发展的阻碍。
一些建议及设想
首先,不奢求C++能为我们提供Socket,数据库这些封装好的类库,我只求C++能为我们提供一个稳定的程序库,提供常用的功能,像boost中的那些库真的应该多加入几个到标准中,这样能极大程度上减轻程序员的负担。也许你说用第三方库也能达到目的,但是如果C++标准规定了,那么我们就能有一个稳定的接口,而且不用为了众多功能相似的库而苦恼。
其次,既然C++0x在语言层次支持了线程同步,那么STL和Boost库没有理由不提供线程安全了吧?!这个是我最期待的特性!
再次,就是争议很大的垃圾回收机制,这个我觉得真的有必要加入进去,想一想在WIndows下如果你自绘界面的话,即使使用shared_ptr、scoped_ptr再配合weak_ptr,那还是肯能导致内存泄漏,所以我希望能加入一个选择性托管的特性。
还有在大部分STL的实现中assert的支持不足,很多时候不知道问题出现在哪,要是多一些assert进行校验,那么会给调试带来很大的方便。
最后这个可能在我有生之年也看不到了,就是把C++中那些有历史问题的函数命名更改成更容易让人理解的,唉。。。
写在最后
以上观点纯属个人理解,请“砖家”和“叫兽”手下留情,如果我真的哪里理解有误,欢迎批评指教。
勘误
关于STL线程安全的问题和大家讨论的最多,我经过实际测试也证实了提供线程安全的非必要性与困难性。
如果我们要提供线程安全,首先考虑容器的安全性,我们对容器的写入和读取都进行Lock,那么这能满足我单次访问的安全性,但是业务逻辑的安全还是需要自己进行Lock,所以这个层面的Lock属于多余的操作,没有用处,且会带来不必要的负担。下面用一段代码说明此种情形:
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
using namespace std;
const int vecCapacity = 100;
int main()
{
typedef vector<int> IntVec;
IntVec vec(vecCapacity);
// 假设vec[]操作是线程安全的, 那么我们能保证如果程序流程在[2]时线程安全吗?
// 答案是不能, 因为后面还跟着一个赋值操作, 程序可能在这里切换线程,
// 如果另外线程对vec进行了一些操作, 其结果肯定不是我们预期的
// 同样, 程序在[1]进行切换的时候, 假设另外的线程修改了vec[i - 1],
// 那么结果必然不是我们的预期行为
// 这只是一个vector<int>就带来了这么多问题, 如果里面不是int而是一些需要
// 进行处理才能使用的对象, 其产生的后果不勘设想
for (int i = 0; i < vecCapacity; ++i) // [1]
vec[i] = i; // [2]
// 这段代码的问题除了上面提到的, 还有几个细节需要注意
// 如果在[3]或者[4]处切换线程, 如果其它线程对vec进行了操作, 导致迭代器失效,
// 那么我们就可能得到错误的结果, 不但是容器内容的错误, 还很可能是业务逻辑错误
for (IntVec::const_iterator iter = vec.begin(); // [3]
vec.end() != iter; ++iter)
cout << *iter << endl; // [4]
return 0;
}
至于对算法进行Lock,和上面的情形类似,都只能保证单次的正确性,不能保证整体上的正确性。还有一种想法是对迭代器进行Lock,这种方案只能解决一小部分问题,如果涉及到将迭代器传入到其它函数时,问题复杂性呈指数级增长,所以此方案也不现实。
所以,给STL提供线程安全不但增加了STL实现的复杂性,也造成了高昂的负担,但是我们期待的线程安全还没实现,所以STL不提供线程安全才是正确的选择。
对于先前的错误假设,我向大家表示抱歉,特此感谢给我指出错误的各位兄弟。