现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

C++ 标准库中的allocator是多余的

2014-11-02 06:35 工业·编程 ⁄ 共 1907字 ⁄ 字号 评论 8 条

我认为C++的allocator是依赖注入的一次失败的尝试。

C/C++里的内存分配和释放是个重要的事情,我同意,在写library的时候,除了默认使用malloc/free,还应该允许用户指定使用内存分配的函数。用现在的话说,如果library依赖于内存分配与释放,就应该允许用户注入这种依赖。我看到有些C library是支持这个的,可以在初始化时传入两个函数指针,指向内存分配和释放的函数。

问题是,allocator是模板参数,而不是构造函数的参数。这意味着

1. 由于不能从构造函数传入allocator,那么每种类型的allocator必须是全局唯一的(Singleton)。无论SGI 的内存池(称为PoolAlloc),还是简单的new wrapper(称为NewAlloc)都只从一个地方(region)搞到内存,这大大限制了其使用。 (补注:这是 SGI STL 的限制,标准 C++ 运行从构造函数传入 allocator,后面的论述依然成立。)

2. allocator是vector类型的一部分,vector<string, PoolAlloc> 和 vector<string, NewAlloc> 是两个类型,不可相互替换。这不仅暴露了实现,还暴露到了类型上,恐怕没有比这更糟糕的了。

下面举例说明,

对于1,假设我有一个任务(假设是parse),需要分配很多小块内存,总容量不超过20M。为了防止内存泄露及避免内存碎片,我希望在任务开始时,先从系统拿到20M内存,供这个任务使用(parse里分配内存只需要改一个指针,释放内存是空操作),等任务完成后,我一次性释放这20M内存,这样既高效又安全。然而C++的allocator并不能帮我实现这一点,因为它是全局的。我不能替换全局的allocator,因为那会影响其他线程。也不能在运行时指定某个vector<string>用哪种allocator,因为类型是编译时确定的。

对于2,如果我想写一个普通的以vector<string>为参数的函数,这不难

void process(vector<string>& records);

由于vector<string, PoolAlloc>和vector<string, NewAlloc>类型不同,process只能接受一种。

但这完全没道理,我不过想访问一个vector<string>,根本不关心它的内存是怎么分配的,却被什么鬼东西allocator挡在了门外。

我要么提供重载:

void process(vector<string, NewAlloc>& records);

void process(vector<string, PoolAlloc>& records);

要么改写成模板:

template<typename Alloc>

void process(vector<string, Alloc>& records);

//(同理可知,如果在一个程序里使用多种allocator,那么所有涉及标准库容器的用户函数都必须改写为函数模板)

无论哪种"解决办法"都会导致代码膨胀,而且给标准库的使用者带来完全不必要的负担。

更糟糕的是,allocator给程序库的作者也带来了不必要的负担。如果想把process(vector<string>& records)放到某个library中,那么为了适应不同的allocator,必须把函数定义放在头文件里(因为这是个函数模板)。明明是针对一个固定类型(vector of string)的函数,却不得不写成函数模板,把实现细节暴露在头文件里,让每个用户都去编译一遍,这真是完全没道理。

根据以上的分析,基本上不可能在一个程序里混用多种allocator,既然一个程序只能有一种allocator,那为什么还要放到每个容器的模板参数里呢?提供一个全局的钩子不就行了嘛?

相反,shared_ptr就只有一个模板参数T,而他同样可以指定allocator----在构造时传入。

现在看来,vector(以及其他标准库容器)与其增加一个Alloc模板参数,不如在构造时传入两个函数指针,一个allocate,一个deallocate,定制的效果也一样。只不过这么做会让标准委员会的人觉得不够GP,很可能被拍掉。

总而言之,allocator并不能达到精确控制(定制)内存分配释放的目的,虽然它名以上是为此而生的。虽然在历史上可能有过正面效果(封装far / near pointer),但现在它无疑就是个累赘。就跟 IOStream 的 Locale/Facet ,auto_ptr 和 valarray 一样,成为C++标准一直要背负的历史包袱。

作者:陈硕

目前有 8 条留言    访客:8 条, 博主:0 条

  1. 爱求索 2014年11月09日 6:35 上午  @回复  Δ1楼 回复

    实际上,所有stl容器的allocator不重载的话,基本数据多了,内存,速度都非常差,基于这样简单的事实,却让加入自己的allocator处理的不够人性化,的确不好。这证明通用性程序的确不是那么好写的,不应该加入形成标准,只应该是C++爱好者的一个参考。

  2. 爱求索 2014年11月09日 6:36 上午  @回复  Δ2楼 回复

    stl这样做的目的主要是为了效率,模板可以静态编译成机器码。如果用虚基类,函数指针回调方式,就有空间上,性能上的一点点开销。静态编译的话,灵活性就缺失了,的确比较麻烦,楼主可以typedef vector myvectror,这样的方法来搞,必竟实际上stl各平台的实现不同,为了通用性,并不是每个平台的STL效率都很高,我也认为stl基本只是个玩具,初学者用用可以,商业化使用有很多不足,很多开源软件是不会用vector,string这些东东的。效率的确不行,于其去熟悉它,还不如自己写,一个方法是数据类型全部用typedef定义。具体使用的地方并不知道是否stl,真的很麻烦的时候,可以逻辑不改,把stl容器给换掉

  3. 爱求索 2014年11月09日 6:36 上午  @回复  Δ3楼 回复

    互换使用不同内存分配器的存储相同元素类型的容器是可能的,把内存分配器写成虚基类而非policy类就能做到,只不过要付出虚函数调用的代价。用这个办法,vector只有一个模板参数,而allocator通过构造函数传入。那么records.clear(), records.push_back()都能正确工作,无论records用的是何种内存分配器(这是面向对象的最基本的使用,一点花招都不耍)。
    stl畏惧虚函数,没有采纳这种方案。而且画蛇添足地把allocator定义成了模板类(并typedef了pointer_type和reference_type等明明可以由容器自己定义的类型)。这和C 的单一内存模型是相悖的。

  4. 爱求索 2014年11月09日 6:36 上午  @回复  Δ4楼 回复

    我不是反对自己设计内存分配器,而是批评STL的allocator设计严重限制了它的使用。
    AutoFreeAlloc是一个一般意义上的内存分配器,实现了多次分配一次释放,满足了文中例子1的要求。
    它能替换vector的allocator吗?换句话说,它能作为vector的第二个模板类型吗?
    这不是主要问题,技术上讲,很容易做一个adapter,让vector也能享受AutoFreeAlloc带来的好处,即让vector用它来分配内存。
    问题是,vector<string, AutoFreeAlloc>并不能兼容原来使用vector<string>的代码,因为类型不同。(另外,就算vector用AutoFreeAlloc来管理内存,它包含的string仍然用全局的std::allocator,除非写成vector<basic_string<char, AutoFreeAlloc>, AutoFreeAlloc> 这里还有char_traits没有写出来)这种把实现细节暴露到类型上的做法实在很让人无语。

  5. 爱求索 2014年11月09日 6:37 上午  @回复  Δ5楼 回复

    至于为什么用Allocator,而不是指定多个参数。我觉得有两个方面的原因:
    一、我们定制allocator的机会不是很多。典型的应用中,不会有多个不同策略的Allocator一起使用。所以,这种为Process写多个重载声明的矛盾不是特别突出。
    二、allocator可以包含一系列的函数,一起打包,不需要每一个都指定,这个同一概念的东西封装在一起的原则。
    三、就算是allocate和deallocate可以使用,你也还是要声明模板形式的函数吧。而且分配以后的内存初始化,你是不是还要指定一个函数做参数吧?既然有这么多函数必须同时指定,还不如把它们打包到一个类中,以一揽子的方式提供呢。

  6. 爱求索 2014年11月09日 6:42 上午  @回复  Δ6楼 回复

    这么说吧,假如我想用 google 的 tcmalloc 来为容器分配内存,因为它的多线程性能更好。那么我当然可以写一个 tcalloc,估计几十行就能写完,然后定义vector<string, tcalloc> 来用。就其本身来看,一切尚好。

    但是,这个 vector<string, tcalloc> 却不能传给我原来写的process(vector<string>& records),因为参数类型不同。

    要想让程序同时能处理vector<T, std::allocator>和vector<T, tcalloc>,除了重写(无论重载还是模板化)我的所有涉及vector的函数,别无他法。

    如果某个第三方库接受vector<string>作为参数,那么我连重写的可能都没有。自定制 allocator 的 vector<string, tcalloc> 就只能自己小范围玩玩,而不能像真正的vector<string>那样给所有的代码用。

    这是现在STL Allocator设计的本质缺陷。

  7. 爱求索 2014年11月09日 6:43 上午  @回复  Δ7楼 回复

    第一:楼主认识有偏颇。因为在替换内存池的时候,不是说去替换std::allocator里面的malloc或是free,而是整个std::allocator。如果看了《泛型编程与STL》就会明白:你要满足这些接口的限制,就行了。也就是楼主不喜欢,自己可以写一个。
    第二:vector<string, PoolAlloc> 和 vector<string, NewAlloc> 是两个类型,不可相互替换。我不知道楼主是怎样认为的,要替换它们实在是再容易不过的事,迭代器就是干这个的事。当然你的满足一定的条件。我认为把vector<string,NewAlloc> 和 vector<string,pollAlooc>相互拷贝是很容易的事。如果楼主需要支持“=”,完全可以重载。当然要自己写这个。
    第三:“同理可知,如果在一个程序里使用多种allocator,那么所有涉及标准库容器的函数都必须改写为函数模板

    我敢说楼主根本没有读过《泛型编程和STL》.STL里面的函

  8. 爱求索 2014年11月09日 6:44 上午  @回复  Δ8楼 回复

    如果楼主看过stl源码剖析中allocatoer的源码,再看看shared_ptr里关于allocator的源码,不知道会不会认为C 对C扩展的部分都是多余的。

给我留言

留言无头像?