认证高级PHP讲师
樓上解釋了為什麼會呼叫拷貝構造函數,我再給你解釋為什麼會亂入。
首先本質原因是vector擴容,一開始容量是0,第一次操作擴容到1,第二次是翻倍為2。
你是用mac下的clang++的,它呼叫的stl實作應該是libcxx,我們可以透過libcxx裡vector的push_back實作原始碼看出來。
原始碼可以在這裡查看 https://github.com/llvm-mirro...
if (this->__end_ != this->__end_cap()) { } else __push_back_slow_path(__x);
__push_back_slow_path的實作是這樣的
allocator_type& __a = this->__alloc(); __split_buffer<value_type, allocator_type&> __v(__recommend(size() + 1), size(), __a); __alloc_traits::construct(__a, _VSTD::__to_raw_pointer(__v.__end_), _VSTD::forward<_Up>(__x)); __v.__end_++; __swap_out_circular_buffer(__v);
可以看出是先擴容,__recommend就是做擴容的工作,隨後把新的內容構造出來放在__a的後半段的首位,注意有forward,所以可移動也可拷貝,最後再執行_ _swap_out_circular_buffer函數。 __swap_out_circular_buffer的實作是這樣的:
template <class _Tp, class _Allocator> void vector<_Tp, _Allocator>::__swap_out_circular_buffer(__split_buffer<value_type, allocator_type&>& __v) { __annotate_delete(); __alloc_traits::__construct_backward(this->__alloc(), this->__begin_, this->__end_, __v.__begin_); _VSTD::swap(this->__begin_, __v.__begin_); _VSTD::swap(this->__end_, __v.__end_); _VSTD::swap(this->__end_cap(), __v.__end_cap()); __v.__first_ = __v.__begin_; __annotate_new(size()); __invalidate_all_iterators(); }
對於迭代器的處理和對於size的處理可以不用看,重點是__alloc_traits::__construct_backward,它負責把之前vector的資料拷貝或移到新vector記憶體區。 __construct_backward的實作是這樣的
while (__end1 != __begin1) { construct(__a, _VSTD::__to_raw_pointer(__end2-1), _VSTD::move_if_noexcept(*--__end1)); --__end2; }
可以從move_if_noexcept看出它要執行移動操作必須保證是noexcept的,而且這個操作是從end開始的,迭代器一直遞減到begin,所以是逆序的。
所以在你執行第一次push_back時,檢查了容量不夠,執行__push_back_slow_path函數,vector擴容到1,並且執行了一次移動構造函數,因為原來的vector是空的,所以不需要進一步處理。
此時就是你的第一次印刷 Move constructor is called. source: hello
Move constructor is called. source: hello
在你執行第二次push_back時,檢查了容量不夠,執行__push_back_slow_path函數,vector擴容到2,並且執行了一次移動構造函數。
此時就是你的第二次列印 Move constructor is called. source: world
Move constructor is called. source: world
隨後它執行__swap_out_circular_buffer,並呼叫__alloc_traits::__construct_backward,由於你的移動建構子不是noexcept的,所以它呼叫了一次你的拷貝建構子。
__swap_out_circular_buffer
__alloc_traits::__construct_backward
此時就是你的第三次印刷 Copy constructor is called. source: hello
Copy constructor is called. source: hello
如果你有更多的元素,你會發現後續的拷貝構造函數執行順序是和原vector的順序反著的,原因上面也說了,操作是從end開始的,迭代器一直遞減到begin。
因為你的移動建構子不是noexcept的。將移動建構函式宣告為noexcept(true),vector就不會呼叫拷貝建構函式了。
vector的push_back可能會需要擴充儲存區。此流程要將原有資料從原始儲存區拷貝到新申請的儲存區。同時push_back需要確保在加入元素的過程中,若有操作拋出異常,容器保持push_back前的狀態。因此,它不能呼叫可能會拋出異常的移動構造函數。因為從語意上來說,移動構造操作中斷(拋出異常)會導致資料損壞。
它會呼叫可能拋異常的拷貝建構函數,是因為從語意上來說這麼做不會導致資料損壞。當然你也可以在拷貝建構函式裡搞事情。
樓上解釋了為什麼會呼叫拷貝構造函數,我再給你解釋為什麼會亂入。
首先本質原因是vector擴容,一開始容量是0,第一次操作擴容到1,第二次是翻倍為2。
你是用mac下的clang++的,它呼叫的stl實作應該是libcxx,我們可以透過libcxx裡vector的push_back實作原始碼看出來。
原始碼可以在這裡查看 https://github.com/llvm-mirro...
__push_back_slow_path的實作是這樣的
可以看出是先擴容,__recommend就是做擴容的工作,隨後把新的內容構造出來放在__a的後半段的首位,注意有forward,所以可移動也可拷貝,最後再執行_ _swap_out_circular_buffer函數。
__swap_out_circular_buffer的實作是這樣的:
對於迭代器的處理和對於size的處理可以不用看,重點是__alloc_traits::__construct_backward,它負責把之前vector的資料拷貝或移到新vector記憶體區。
__construct_backward的實作是這樣的
可以從move_if_noexcept看出它要執行移動操作必須保證是noexcept的,而且這個操作是從end開始的,迭代器一直遞減到begin,所以是逆序的。
所以在你執行第一次push_back時,檢查了容量不夠,執行__push_back_slow_path函數,vector擴容到1,並且執行了一次移動構造函數,因為原來的vector是空的,所以不需要進一步處理。
此時就是你的第一次印刷
Move constructor is called. source: hello
在你執行第二次push_back時,檢查了容量不夠,執行__push_back_slow_path函數,vector擴容到2,並且執行了一次移動構造函數。
此時就是你的第二次列印
Move constructor is called. source: world
隨後它執行
__swap_out_circular_buffer
,並呼叫__alloc_traits::__construct_backward
,由於你的移動建構子不是noexcept的,所以它呼叫了一次你的拷貝建構子。此時就是你的第三次印刷
Copy constructor is called. source: hello
如果你有更多的元素,你會發現後續的拷貝構造函數執行順序是和原vector的順序反著的,原因上面也說了,操作是從end開始的,迭代器一直遞減到begin。
因為你的移動建構子不是noexcept的。將移動建構函式宣告為noexcept(true),vector就不會呼叫拷貝建構函式了。
vector的push_back可能會需要擴充儲存區。此流程要將原有資料從原始儲存區拷貝到新申請的儲存區。同時push_back需要確保在加入元素的過程中,若有操作拋出異常,容器保持push_back前的狀態。因此,它不能呼叫可能會拋出異常的移動構造函數。因為從語意上來說,移動構造操作中斷(拋出異常)會導致資料損壞。
它會呼叫可能拋異常的拷貝建構函數,是因為從語意上來說這麼做不會導致資料損壞。當然你也可以在拷貝建構函式裡搞事情。