C++?STL標(biāo)準(zhǔn)庫(kù)std::vector擴(kuò)容時(shí)進(jìn)行深復(fù)制原因詳解
引子
但是筆者卻發(fā)現(xiàn)了一個(gè)奇怪的現(xiàn)象,std::vector擴(kuò)容時(shí),對(duì)其中的元素竟然進(jìn)行的是深復(fù)制。請(qǐng)看示例代碼:
#include <iostream>
#include <vector>
struct Test {
Test() {std::cout << "Test" << std::endl;}
~Test() {std::cout << "~Test" << std::endl;}
Test(const Test &) {std::cout << "Test copy" << std::endl;}
Test(Test &&) {std::cout << "Test move" << std::endl;}
};
int main(int argc, const char *argv[]) {
std::vector<Test> ve;
ve.emplace_back();
ve.emplace_back();
ve.emplace_back();
return 0;
}
打印結(jié)果如下:
Test
Test
Test copy
~Test
Test
Test copy
Test copy
~Test
~Test
~Test
~Test
~Test
由于我們沒(méi)有調(diào)用reverse函數(shù),所以默認(rèn)只分配了一個(gè)元素的大小。第一次emplace_back時(shí),僅進(jìn)行了一次普通構(gòu)造。第二次emplace_back時(shí),就需要進(jìn)行擴(kuò)容,然后把第一個(gè)元素拷貝過(guò)去,再釋放原來(lái)的對(duì)象。所以這里除了有一次新的構(gòu)造以外,還有一次復(fù)制和釋放。后面的行為類(lèi)似,不再贅述,
但關(guān)鍵問(wèn)題就在于,Test類(lèi)明明實(shí)現(xiàn)了移動(dòng)構(gòu)造(淺復(fù)制),可這里竟然調(diào)用了拷貝構(gòu)造(深復(fù)制)。
如果vector擴(kuò)容無(wú)腦調(diào)用拷貝構(gòu)造,那么這個(gè)對(duì)象如果含有很多外鏈的成員(比如說(shuō)指向buffer的指針、指向其他對(duì)象的指針等),調(diào)用拷貝構(gòu)造就意味著要把這些鏈接的對(duì)象全部都重新構(gòu)造一遍。這對(duì)于vector自身擴(kuò)容來(lái)說(shuō),顯然是沒(méi)有必要的,會(huì)極度浪費(fèi)內(nèi)存空間。
查找原因
基于上述理由,我認(rèn)為STL的開(kāi)發(fā)者不可能連這個(gè)問(wèn)題都考慮不到,但想不通為什么我明明實(shí)現(xiàn)了移動(dòng)構(gòu)造,卻不能調(diào)用。
帶著這樣的疑問(wèn)我去研讀了STL的源碼(GNU版本),在vector擴(kuò)容時(shí),會(huì)調(diào)用_M_realloc_insert函數(shù),該函數(shù)在vector.tcc文件中實(shí)現(xiàn)。在這個(gè)函數(shù)里面對(duì)已有元素進(jìn)行拷貝的時(shí)候,看到了類(lèi)似這樣的代碼:
__new_finish = std::__uninitialized_move_if_noexcept_a (__old_start, __position.base(), __new_start, _M_get_Tp_allocator()); ++__new_finish;
有趣的就是這個(gè)__uninitialized_move_if_noexcept_a,我們找到這個(gè)函數(shù)的實(shí)現(xiàn):
template<typename _InputIterator, typename _ForwardIterator,
typename _Allocator>
inline _ForwardIterator
__uninitialized_move_if_noexcept_a(_InputIterator __first,
_InputIterator __last,
_ForwardIterator __result,
_Allocator& __alloc)
{
return std::__uninitialized_copy_a
(_GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(__first),
_GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(__last), __result, __alloc);
}再看一下_GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR的實(shí)現(xiàn)
#if __cplusplus >= 201103L #define _GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(_Iter) std::__make_move_if_noexcept_iterator(_Iter) #else #define _GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(_Iter) (_Iter) #endif // C++11
也就是說(shuō),在C++11以前,這玩意就是對(duì)象本身(畢竟C++11以前還沒(méi)有移動(dòng)構(gòu)造),而在C++11以后被定義成了__make_move_if_noexcept_iterator,繼續(xù)查看其定義。
template<typename _Iterator, typename _ReturnType
= typename conditional<__move_if_noexcept_cond
<typename iterator_traits<_Iterator>::value_type>::value,
_Iterator, move_iterator<_Iterator>>::type>
inline _GLIBCXX17_CONSTEXPR _ReturnType
__make_move_if_noexcept_iterator(_Iterator __i)
{ return _ReturnType(__i); }
這里用了一個(gè)conditional,來(lái)判斷這個(gè)迭代器的類(lèi)型,如果__move_if_noexcept_cond為真,就取迭代器本身,否則就取移動(dòng)迭代器。看起來(lái)問(wèn)題就在這里了,之前我們的例程中的Test一定就是符合了這個(gè)__move_if_noexcept_cond,導(dǎo)致用了原始迭代器。
繼續(xù)深挖這個(gè)__move_if_noexcept_cond,看到這樣的代碼:
template<typename _Tp>
struct __move_if_noexcept_cond
: public __and_<__not_<is_nothrow_move_constructible<_Tp>>,
is_copy_constructible<_Tp>>::type { };也就是說(shuō),如果一個(gè)類(lèi),不存在不會(huì)拋出異常的移動(dòng)構(gòu)造函數(shù)并且可拷貝,那么就為真。
Test類(lèi)顯然符合,所以vector<Test>在復(fù)制時(shí)用了普通的迭代器進(jìn)行了遍歷,自然就會(huì)調(diào)用拷貝構(gòu)造函數(shù)進(jìn)行復(fù)制了。
解決方法
所以,我們需要讓Test不符合__move_if_noexcept_cond的條件,也就是這里要將移動(dòng)構(gòu)造函數(shù)聲明為noexcept表示它不會(huì)拋出異常,這樣vector<Test>在復(fù)制時(shí)就會(huì)使用移動(dòng)迭代器(就是會(huì)包裝一層std::move),從而觸發(fā)移動(dòng)構(gòu)造。
順道我們也看一眼移動(dòng)迭代器的原理:
template<typename _Iterator>
class move_iterator {
_Iterator _M_current;
// ...
public:
using iterator_type = _Iterator;
explicit _GLIBCXX17_CONSTEXPR
move_iterator(iterator_type __i)
: _M_current(std::move(__i)) { }
// ...
}確實(shí)調(diào)用了std::move,證明我們的思路沒(méi)錯(cuò)。
所以,修改Test代碼,實(shí)現(xiàn)noexcept移動(dòng)構(gòu)造:
struct Test {
long a, b, c, d;
Test() {std::cout << "Test" << std::endl;}
~Test() {std::cout << "~Test" << std::endl;}
Test(const Test &) {std::cout << "Test copy" << std::endl;}
Test(Test &&) noexcept {std::cout << "Test move" << std::endl;}
};
int main(int argc, const char *argv[]) {
std::vector<Test> ve;
ve.emplace_back();
ve.emplace_back();
ve.emplace_back();
return 0;
}打印結(jié)果如下:
Test
Test
Test move
~Test
Test
Test move
Test move
~Test
~Test
~Test
~Test
~Test
這次如我們所愿,調(diào)用了移動(dòng)構(gòu)造。
結(jié)論
STL中考慮到異常的情況,因此,像這種容器內(nèi)部的復(fù)制行為,是要求不能夠發(fā)生異常的,因此,只有當(dāng)移動(dòng)構(gòu)造函數(shù)聲明為noexcept的時(shí)候才會(huì)調(diào)用,否則將統(tǒng)一調(diào)用拷貝構(gòu)造函數(shù)。
然而,在移動(dòng)構(gòu)造函數(shù)中本來(lái)就不應(yīng)該拋出異常,因此,在大多數(shù)情況下,移動(dòng)構(gòu)造函數(shù)都應(yīng)該用noexcept來(lái)聲明。
到此這篇關(guān)于C++ STL標(biāo)準(zhǔn)庫(kù)std::vector擴(kuò)容時(shí)進(jìn)行深復(fù)制原因詳解的文章就介紹到這了,更多相關(guān)C++ std::vector內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java實(shí)現(xiàn)任意四則運(yùn)算表達(dá)式求值算法
這篇文章主要介紹了java實(shí)現(xiàn)任意四則運(yùn)算表達(dá)式求值算法,實(shí)例分析了基于java實(shí)現(xiàn)表達(dá)式四則運(yùn)算求值的原理與技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04
C++ Boost Coroutine使用協(xié)程詳解
通過(guò)Boost.Coroutine,可以在C++中使用協(xié)程。協(xié)程是其他編程語(yǔ)言的一個(gè)特性,通常使用關(guān)鍵字yield來(lái)表示協(xié)程。在這些編程語(yǔ)言中,yield可以像return一樣使用2022-11-11
C語(yǔ)言二叉排序樹(shù)的創(chuàng)建,插入和刪除
本文主要介紹了Java實(shí)現(xiàn)二叉排序樹(shù)的查找、插入、刪除、遍歷等內(nèi)容。具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2021-10-10
C++實(shí)現(xiàn)LeetCode(118.楊輝三角)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(118.楊輝三角),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
解決codeblocks致命錯(cuò)誤:openssl/aes.h:沒(méi)有這樣的文件或目錄問(wèn)題
這篇文章主要介紹了解決codeblocks致命錯(cuò)誤:openssl/aes.h:沒(méi)有這樣的文件或目錄問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06
C語(yǔ)言嵌套鏈表實(shí)現(xiàn)學(xué)生成績(jī)管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言嵌套鏈表實(shí)現(xiàn)學(xué)生成績(jī)管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07
C語(yǔ)言實(shí)現(xiàn)24點(diǎn)游戲源代碼
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)24點(diǎn)游戲源代碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10

