標籤:目標 避免 開始 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 自訂指南