C++中的内存分配器std::allocator

-- TOC --

前段时间,我在公司咨询一位C++专家,问题是,是否有办法可以手动调用某个对象的destructor?他回答说,没有。delete对象指针,实际上是调用destructor后并且释放空间,而且delete只能跟指针类型。

最近学习std::allocator时,发现通过这个类型,可以实现手动调用对象destructor。


再后来,学习到了operator new/delete,上诉问题完美解决,也铺垫了下面的内容。


new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,我们几乎肯定知道对象应有什么值。但当分配一大块内存时,我们通常是计划在这块内存上按需构造对象,STL中的container,都有这样的需求。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以先分配大块内存,但只在真正需要时才执行对象的创建操作(同时付出一定开销)。有的时候,将内存分配和对象构造组合在一起可能会导致不必要的浪费。标准库allocator类定义在头文件<memory>中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

#include <iostream>
#include <memory>
using namespace std;


struct xyz {
    xyz(void) { cout << "xyz constructor\n"; }
    xyz(const xyz *x) { cout << "xyz pointer copy constructor\n"; }
    xyz(const xyz &x) { cout << "xyz copy constructor\n"; }
    ~xyz(void) { cout << "xyz destructor\n"; }
};


int main(void) {
    allocator<xyz> xyzpool;
    auto a {xyzpool.allocate(4)};
    xyzpool.construct(a++);
    xyzpool.construct(a++);
    xyzpool.construct(a, a-1); // pointer copy constructor
    ++a;
    xyzpool.construct(a, *(a-1));  // copy constructor
    xyzpool.destroy(a--);
    xyzpool.destroy(a--);
    xyzpool.destroy(a--);
    xyzpool.destroy(a);
    xyzpool.deallocate(a,3);
    return 0;
}

xyzpool为包含类型为xyz的allocator,a指向包含3块xyz对象的未初始化的内存(类型应该是xyz*),然后构造4个xyz对象,xyzpool.construct(a,a-1)表示用a-1位置的对象构造a位置的对象(需要对象支持pointer copy接口)。然后手动销毁4个xyz对象,最后释放全部内存。注意,要先destroy,再deallocate!

以上代码的输出为:

xyz constructor
xyz constructor
xyz pointer copy constructor
xyz copy constructor
xyz destructor
xyz destructor
xyz destructor
xyz destructor

本文最初的问题已经得到解答,将对象放入allocator中,就可以实现手动创建和销毁,与对象内存的申请和释放分离。

下面测试4个helper接口的测试:

#include <iostream>
#include <vector>
#include <memory>
using namespace std;


int main(void) {
    vector<int> data {1,2,3,4,5,6,7,8};
    allocator<int> dalloc;
    auto p {dalloc.allocate(data.size())};

    uninitialized_copy(data.begin(), data.end(), p);
    for (int i=0; i<data.size(); ++i) {
        cout << *(p+i) << " ";
        dalloc.destroy(p+i);
    }
    cout << endl;

    uninitialized_copy_n(data.begin(), 4, p);
    uninitialized_fill(p+4, p+6, 0);
    uninitialized_fill_n(p+6, 2, 9);
    for (int i=0; i<data.size(); ++i) {
        cout << *(p+i) << " ";
        dalloc.destroy(p+i);
    }
    cout << endl;

    dalloc.deallocate(p, data.size());
    return 0;
}

输出:

1 2 3 4 5 6 7 8 
1 2 3 4 0 0 9 9

我们需要对C++的allocator的堆内存接口调用顺序有个清晰的认识,如下图所示:

allocator.png

从C++20开始,construct和destroy接口就不再出现在allocator里了,只有allocator_traits中还保存。。(***_traits模板类是一个中间层!)

我们完全可以用placement new来重写前面的测试代码:

#include <iostream>
#include <memory>
using namespace std;


struct xyz {
    xyz(void) { cout << "xyz constructor\n"; }
    ~xyz(void) { cout << "xyz destructor\n"; }
};


int main(void) {
    allocator<xyz> xyzpool;
    auto a {xyzpool.allocate(3)};
    new(a++) xyz{};
    new(a++) xyz{};
    new(a) xyz{};
    (a--)->~xyz();
    (a--)->~xyz();
    a->~xyz();
    xyzpool.deallocate(a,3);
    return 0;
}

首先看问题1,为什么要有allocator?vector自己分配空间,自己调整策略不就完事了吗,为什么麻烦地弄个allocator出来?OK,如果STL只有一个容器,的确可以这样做,但STL有很多容器,vector需要分配释放、构造销毁,map也需要,set也需要,deque也需要,所有的容器都需要,作为一个程序员,当这个需求摆在面前的时候,应该怎么做,不需要过多考虑了吧?既然这是个通用的东西,我们当然要设计一个通用的模块。而且稍微细想,内存分配这种东西,和容器关系也不大,不是吗?所以我们就把内存分配释放、构造销毁的操作独立出来,叫它什么呢?就叫allocator吧!于是,allocator出现了。

再看问题2,allocator出现了,没错,但为什么要作为模板参数开放给使用者?我只用vector,我根本不关系它的allocator是谁啊!从最简单的使用角度来看,这么说没错,STL自己独立封装allocator就可以了,不必让开发者自己传入参数,但如果一个项目有自己的内存管理策略,同时又使用了STL库,这个时候如果不能控制STL的内存分配,那么STL的内存对于项目内存管理器来说就是黑洞,也无法进行相关优化。解决方法?很简单,把分配器作为参数开放出去,给个默认值,外部如果想要接管内存处理,只要按照格式提供定制的allocator就可以了,如果不想接管,用默认的就行!

本文链接:https://cs.pynote.net/sf/c/cpp/202211053/

-- EOF --

-- MORE --