std::allocator在stl容器中使用問題

std::allocator常用於stl中的各種容器。對應的,stl的容器中也提供了相應的內存分配器參數。當需要統計內存的使用或者自定義內存分配時,十分有用。以std::vector爲例:

// std=c++11
// https://www.cplusplus.com/reference/vector/vector/vector/

template < class T, class Alloc = allocator<T> > class vector;

explicit vector (const allocator_type& alloc = allocator_type());
explicit vector (size_type n);
         vector (size_type n, const value_type& val,
                 const allocator_type& alloc = allocator_type());
template <class InputIterator>
  vector (InputIterator first, InputIterator last,
          const allocator_type& alloc = allocator_type());
	
vector (const vector& x);
vector (const vector& x, const allocator_type& alloc);
	
vector (vector&& x);
vector (vector&& x, const allocator_type& alloc);
	
vector (initializer_list<value_type> il,
       const allocator_type& alloc = allocator_type());

可以看到,有兩個地方可以使用分配器,一個是聲明vector時的模板參數,另一個是構造vector對象時的構造參數alloc。通常我會覺得這個很簡單,但是最近在項目中發現自定義的內存分配器沒生效,才發現踩了一些坑。原因是有兩個地方可以使用分配器,那麼除去都不使用分配器的情況,則有 2 * 2 - 1 = 3 種使用情況,在一些巧合的原因下,會產生一些意想不到的結果。

#include <vector>
#include <iostream>

template <class T>
class StlAlloc : public std::allocator<T>
{
public:
    using value_type = T;
    using size_type = size_t;

    template <class U>
    struct rebind
    {
        using other = StlAlloc<U>;
    };
public:
    StlAlloc() = default;
    ~StlAlloc() = default;

    T *allocate(size_type n, std::allocator<void>::const_pointer hint=0)
    {
        std::cout << __FUNCTION__ << "  " << n << "  " << this << std::endl;
        return static_cast<T *>(operator new(sizeof(T) * n));
    }

    void deallocate(T *p, size_type n)
    {
        operator delete(p);
    }
};

int main()
{
    // 情景1:僅模板參數使用分配器
    std::vector<int, StlAlloc<int>> v;
    v.resize(1024, 0);

    std::vector<int, StlAlloc<int>> v2;
    v2.resize(1024, 0);

    // 情景2:模板參數和構造參數均使用分配器
    StlAlloc<int> alloc;

    std::vector<int, StlAlloc<int>> v3(alloc);
    v3.resize(1024, 0);

    std::vector<int, StlAlloc<int>> v4(alloc);
    v4.resize(1024, 0);


    // 情景3:僅構造參數均使用分配器
    std::vector<int> v5(alloc);
    v5.resize(1024, 0);

    std::vector<int> v6(alloc);
    v6.resize(1024, 0);

    return 0;
}

在線運行 結果

allocate  1024  0x77b21dc9db20
allocate  1024  0x77b21dc9db40
allocate  1024  0x77b21dc9db60
allocate  1024  0x77b21dc9db80

僅模板參數使用分配器

std::vector<int, StlAlloc<int>> v;
v.resize(1024, 0);

std::vector<int, StlAlloc<int>> v2;
v2.resize(1024, 0);

和預期的結果一致,每個對象都使用構造函數vector (const allocator_type& alloc = allocator_type())根據模板參數allocator_type創建了一個分配器,因此打印出以下兩行日誌,每個分配器的地址都不一樣

allocate  1024  0x77b21dc9db20
allocate  1024  0x77b21dc9db40

模板參數和構造參數均使用分配器

StlAlloc<int> alloc;

std::vector<int, StlAlloc<int>> v3(alloc);
v3.resize(1024, 0);

std::vector<int, StlAlloc<int>> v4(alloc);
v4.resize(1024, 0);

一直以爲,當在構造參數傳入分配器時,vector會使用此分配器,而不再額外創建分配器。然而,從日誌來看

allocate  1024  0x77b21dc9db60
allocate  1024  0x77b21dc9db80

分配器的地址是不一樣的。根據www.cplusplus.com的描述

alloc
    Allocator object.
    The container keeps and uses an internal copy of this allocator.

即使傳入了分配器,也會執行拷貝。而一般來說,自定義的內存分配器都是希望多個對象共用同一個內存分配器的,這樣內存利用率高,這就需要額外處理了,比如說在allocate函數裏調用全局的內存池。

僅構造參數均使用分配器

std::vector<int> v5(alloc);
v5.resize(1024, 0);

std::vector<int> v6(alloc);
v6.resize(1024, 0);

這其實是一種錯誤的用法,一般不會這樣寫。之所以說這個用例是因爲項目中的舊代碼改漏了,結果發現內存統計的時候完全沒統計到對應的內存分配,而編譯運行卻沒有問題,排查後才發現問題。默認情況下,stl的容器使用std::allocator分配內存,上面的例子中,因爲繼承了std::allocator,所以傳入的alloc被轉換爲基類std::allocator,而且會執行一份拷貝,那最終得到的分配器類型就是std::allocator所以沒有任何日誌輸出,也沒有報錯。

把例子中的class StlAlloc改成不繼承std::allocator就會因爲傳入的參數和聲明時分配器的參數不一致編譯報錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章