C++ allocator 自訂指南

來源:互聯網
上載者:User

標籤:目標   避免   開始   ace   生命週期   成員   表示   tor   uniq   

閑話

昨天培神在群裡抱怨說自訂allocator遇到了奇怪的問題,然後選擇了pmr,我表示很理解。allocator這個東西,出生時就伴隨著設計錯誤和無用的抽象,C++03-14糊了這麼久,甚至還加了新feature來相容舊翔和糊新翔,結果C++17最終還是另立門派搞了個pmr。

簡單說,雖然allocator的concept說了很多東西,也有一些周邊的concept比如allocator aware container和語言設施如allocator_traits的支援,allocator的自訂依然收到了極大的限制。

我對此的結論是,雖然C++11開始標準自稱支援stateful allocator,但從各種各樣的曆史和標準庫實現隱含的限定中推導得到,對allocator的自訂只能是stateless的。從實現上講,allocator裡最多隻能放一個指標。

容器的構造強制要求allocator支援拷貝和轉換

allocator作為容器的構造參數,是被拷貝進容器的,而對於一個有狀態的allocator,它的拷貝不一定是合理的。其次,容器可能使用rebind獲得另一個allocator類型,然後使用傳入的allocator來轉換構造,這種轉換構造實際上也是一種拷貝,而,拷貝不一定合理。再次,各大標準庫都會在debug版本的容器中儲存一些元資訊,而這些元資訊佔用的記憶體,也是使用allocator分配的,所以往往在debug模式下,容器實現需要兩個allocator,一個是allocator<value_type>,另一個是allocator<metadata>,容器會在需要分配元資訊的位置用allocator<value_type>當場轉換構造出allocator<metadata>並使用(比如MSVC),所以你可能會在容器中看到如下代碼

//片段1
void some_container::check_some_metadata() {#ifdef _DEBUG    allocator<metadata> _metaal(this->_allocator); //當場構造    _metaal.allocate(...); //使用    _metaal.deallocate(...); //使用#endif}

對於有狀態allocator,拷貝不一定合理,以stack_allocator為例,他可能有兩種實現,一種是內部裝一個定長數組的

struct stack_allocator {    byte _stack[MAX_SIZE];    byte* _stack_ptr;};

另一種是內部裝有指向外部固定空間的指標的

struct stack_allocator {    byte* _stack_bottom;    byte* _stack_top;};

第一種實現完全無法拷貝,因為它只能被使用者定義在確定的位置上,由使用者保證他的生命週期大於等於所分配的記憶體的生命週期,如果允許了它的拷貝,在片段1的代碼中就會出現問題。因為函數返回後空間就不複存在。

第二種實現和第一種一樣不可行,首先,淺拷貝是不合理的,如果你使用副本allocator分配了空間,副本的stack_top指標移動到了新的位置,而本體的指標卻沒有變化,那下一次使用本體來分配空間,也會出現問題。

為此,C++標準特別規定,allocator拷貝(或轉換)之後,兩個allocator分配的空間必須能互相釋放,進一步確認了allocator無法有狀態的事實。

即使你放棄了調試便利,使用了宏等條件編譯選項禁用了容器內的debug資訊,保證了容器只使用一個allocator,依然不能解決問題。這個問題來源於我們提到過的,allocator被拷貝進容器,以及rebind的存在。你可能想通過不提供allocator參數,讓容器通過預設構造的方式來構造allocator來避免拷貝,然而這樣依然不可能,以MSVC的容器為例,在沒有提供allocator時,容器的最外層建構函式會預設構造容器,然後將他拷貝給內部實現,就像如下的虛擬碼那樣

template<class T, class Al>class vector_base { //內部實現    vector(const Al& a)        : _allocator(a) //拷貝    {}    //...};template<class T, class Al = /*...*/>class vector : vector_base<T, Al> //內部實現{    vector() :        vector_base(Al())    {}    //...}

對於map這種value_type和allocator實際分配的東西不一樣的情況,實現也會從建構函式接受(或預設構造)一個allocator<value_type>然後將他傳給rebind得到的allocator<node>來進行轉換構造,也不行。

看起來在這種情況下,有狀態allocator只能通過引用計數來共用狀態才能實現了,這就是我上面所說的結論,allocator裡最多放一個指標了。

諷刺的是,容器的的拷貝構造和拷貝賦值卻沒有對allocator的拷貝性質提出要求,實現會通過allocator_traits判斷allocator是否可以拷貝(propagate_on_container_copy_construction),對於不能拷貝的allocator,移動構造(賦值)不會直接進行內部指標的交換,而是像拷貝構造(賦值)那樣,在目標容器預留夠空間,然後將元素一個一個move過去。可是這設計有p用,你根本寫不出來不能拷貝的allocator。

主從allocator無法實現

對於這種不能拷貝的情況,我曾經構想過一種hack,是通過特殊實現allocator的rebind,讓allocator::rebind<U>返回一個ref_allocator,通過它來引用主allocator,進而達到不通過引用計數來讓rebind後的allocator和主allocator共用狀態的目的(如下面代碼),這樣子看似可以解決片段1中的問題。然而依然有其他的問題沒有解決,那就是對於map, set這樣的容器,容器內部只會儲存rebind後的allocator_ref,而不會儲存主allocator,這樣你就只能自己手動控制在外面噹啷著的allocator的生命週期要隨容器一起,這麼做並不好。而且,這樣的實現還禁用了容器的預設構造,因為預設構造的allocator<value_type>用來構造allocator_ref後就結束了生命週期,此時allocator_ref內部儲存的是主allocator的懸掛引用。

//主從allocator設計示意template<class U, class T>struct allocator_ref {    allocator<T>* _ref;        template<class U2>    struct rebind {        using other = allocator_ref<U2, T>;    };};template<class T>struct allocator {    template<class U>    struct rebind {        using other = allocator_ref<U, T>;    };};
fancy pointer也不是那麼fancy

allocator::pointer可以是一個自訂的fancy pointer,並且容器的實現也假定了allocator可能使用fancy pointer,比如MSVC的string裡面那個union就寫了空的建構函式來支援其中的pointer成員是對象的情況。然而fancy pointer依然不能是有狀態的,標準要求fancy pointer必須能和它指向的對象的裸指標無痛轉換,所以shared_ptr不是fancy_pointer,unique_ptr勉強算。你想通過fancy pointer來封裝某些複雜抽象的希望又破滅了。

allocator::construct除了placement new外幹不了別的

construct函數接受變長參數,在給定指標上構造對象,這個函數你以為它有很大的擴充空間?實際上他幾乎不能在物件建構上做太多文章,我本想通過它來實現在fancy pointer上自訂構造,可他從C++11開始接受的就是裸指標了,就算你強行不寫裸指標作為參數,allocator_traits在轉寄costruct調用的時候傳給你的也是裸指標,沒有辦法用它來實現fancy pointer上的自訂構造。

其次,allocator<T>的construct不能只用來構造T,這讓想打細算地嘗試根據T的類型來壓縮分配記憶體的空間的想法變得不可能。為什麼不能只用來構造T呢,還是rebind,以MSVC的map為例,rebind得到的allocator<node>分配的是node類型,然後實現會用allocator<node>的construct在node類型的value_type成員上construct那個pair,完球,這還玩毛。

你還能對construct抱什麼自訂的希望呢?也就用cout打個log了吧,儲存幾個偵錯項目都做不到,因為他所在的allocator不能有狀態。

標準庫怎麼做的?pmr

pmr幹了什嗎?你自己在別處開個memory_resource,然後pmr::allocator裡面裝個指標去引用它。好了,無狀態,支援拷貝和轉換,支援自訂分配,實現簡單,容器不會因為allocator類型不一樣無法拷貝,相容舊標準代碼,相容scoped_allocator_adapter,簡直完美。

就是有個虛函數很不爽。

C++ allocator 自訂指南

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.