STL欠缺什么?

写这种文章估计又要被一些"砖家"拍砖了,但是今天突然想反思一下这段时间剖析的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不提供线程安全才是正确的选择。

对于先前的错误假设,我向大家表示抱歉,特此感谢给我指出错误的各位兄弟。

  • 7
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 142
    评论
STL(Standard Template Library)是C++标准库的一部分,它提供了一系列高效、灵活的数据结构和算法,用于处理动态数据。在STL中,常见的容器主要包括以下几种: 1. **序列容器**(Sequence Containers): - `std::vector`:动态数组,支持随机访问。 - `std::deque`:双端队列,可以在两端进行高效的插入和删除操作。 - `std::list`:双向链表,元素按插入顺序排列,但查找效率较低。 - `std::forward_list`:单向链表,类似于`list`,但不支持在任意位置插入或删除。 - `std::array`:固定大小的数组,类似C语言中的数组。 2. **关联容器**(Associative Containers): - `std::map`(或`std::unordered_map`):关联键值对,使用哈希表实现高效查找。 - `std::set`(或`std::unordered_set`):无序的键集合,不允许重复。 - `std::multiset`:有序的键集合,允许重复。 - `std::multimap`:关联键值对的多值集合,允许多个键对应同一值。 3. **堆容器**(Priority Container): - `std::priority_queue`:堆数据结构,常用于实现优先级队列。 4. **集合容器**(Set-like Containers): - `std::set`:无序集合,使用哈希表实现。 - `std::unordered_set`:无序且无重复的集合。 5. **容器适配器**(Container Adapters): - `std::stack`:栈,基于`vector`或`deque`实现。 - `std::queue`:队列,同样基于`vector`或`deque`实现。 - `std::bitset`:位集,表示一系列二进制位。 STL迭代器是一种抽象概念,它是容器和算法之间通用的接口,使得我们能够遍历容器中的元素,而不必关心底层的具体实现细节。迭代器提供了读取和修改容器元素的方法,可以指向容器的开始、结束和中间位置。无论是序列还是关联容器,都有相应的迭代器类型,如`iterator`和`const_iterator`等,分别用于读写操作。迭代器的生命周期管理也非常重要,确保它们不会超出容器的有效范围。
评论 142
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值