昨天培神在羣裏抱怨說自定義allocator遇到了奇怪的問題,而後選擇了pmr,我表示很理解。allocator這個東西,出生時就伴隨着設計錯誤和無用的抽象,C++03-14糊了這麼久,甚至還加了新feature來兼容舊翔和糊新翔,結果C++17最終仍是另立門派搞了個pmr。node
簡單說,雖然allocator的concept說了不少東西,也有一些周邊的concept好比allocator aware container和語言設施如allocator_traits的支持,allocator的自定義依然收到了極大的限制。數組
我對此的結論是,雖然C++11開始標準自稱支持stateful allocator,但從各類各樣的歷史和標準庫實現隱含的限定中推導獲得,對allocator的自定義只能是stateless的。從實現上講,allocator裏最多隻能放一個指針。less
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爲例,他可能有兩種實現,一種是內部裝一個定長數組的this
struct stack_allocator { byte _stack[MAX_SIZE]; byte* _stack_ptr; };
另外一種是內部裝有指向外部固定空間的指針的debug
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時,容器的最外層構造函數會默認構造容器,而後將他拷貝給內部實現,就像以下的僞代碼那樣code
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。
對於這種不能拷貝的狀況,我曾經構想過一種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>; }; };
allocator::pointer能夠是一個自定義的fancy pointer,而且容器的實現也假定了allocator可能使用fancy pointer,好比MSVC的string裏面那個union就寫了空的構造函數來支持其中的pointer成員是對象的狀況。然而fancy pointer依然不能是有狀態的,標準要求fancy pointer必須能和它指向的對象的裸指針無痛轉換,因此shared_ptr不是fancy_pointer,unique_ptr勉強算。你想經過fancy pointer來封裝某些複雜抽象的但願又破滅了。
construct函數接受變長參數,在給定指針上構造對象,這個函數你覺得它有很大的擴展空間?實際上他幾乎不能在對象構造上作太多文章,我本想經過它來實如今fancy pointer上自定義構造,可他從C++11開始接受的就是裸指針了,就算你自定義的construct強行不寫裸指針做爲參數,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,也就是說會有如allocator<node>.construct<value_type>(value_type*)這樣的調用,這還玩毛。
你還能對construct抱什麼自定義的但願呢?頂多就用cout打個log了吧,儲存幾個調試對象都作不到,由於咱們前面說過了,allocator不能有狀態。
pmr幹了什麼?你本身在別處開個memory_resource,而後pmr::allocator裏面裝個指針去引用它。好了,無狀態,支持拷貝和轉換,支持自定義分配,實現簡單,容器不會由於allocator類型不同沒法拷貝,兼容舊標準代碼,兼容scoped_allocator_adapter,簡直完美。
就是有個虛函數很不爽。