STL中map使用陷阱
- 問題描述
最近在專案開發中使用到了SGI版本的STL中的map,結果遇到了非預期的現象。
- 問題模擬
眾所周知,map的底層是用紅黑樹來管理key-value關係的,因此在find時,效率極高,但同樣這也帶來了某些非預期的空間開銷。首先就程式碼中的使用模型簡化如下:
#include <iostream>
#include <string>
#include <map>
#include <set>
using namespace std;
enum PetType
{
CAT,
DOG
};
class PetHouse
{
public:
PetHouse()
{
cout << "construct PetHouse ... : " << this << endl;
}
~PetHouse()
{
cout << "destruct PetHouse ... : " << this << endl;
}
};
class Pet
{
public:
Pet()
{
mPetHouse = new PetHouse;
}
~Pet()
{
if (NULL != mPetHouse)
{
delete mPetHouse;
mPetHouse = NULL;
}
}
public:
void Adopt(string petName)
{
mPets.insert(petName);
}
private:
PetHouse *mPetHouse;
set<string> mPets;
};
class Lady
{
public:
Lady()
{
cout << "coustruct Lady ... : " << this << endl;
}
~Lady()
{
cout << "destruct Lady ... : " << this << endl;
}
public:
void AdoptPet(PetType type, string petName)
{
const static char *TYPE[] = {"cat", "dog"};
Pet *ptr = &mPet[type]; // // // // // // #1
ptr->Adopt(petName);
cout << "adopt a " << TYPE[type]
<< ", name: " << petName << endl;
}
private:
map<PetType, Pet> mPet; // // // // // // // #2
};
int main()
{
Lady myWife;
myWife.AdoptAPet(CAT, "cat0");
myWife.AdoptAPet(CAT, "cat1");
myWife.AdoptAPet(DOG, "dog0");
myWife.AdoptAPet(DOG, "dog1");
return 0;
}
- 程式碼分析
注意,程式中#2
定義map時,map的值為Pet型別;此外,對#1
處的map過載的[]
作一說明:當map存在待索引的key時,改中括號運算子的結果會返回已存在的key對應的value,否則,新建一對key-value儲存到map中並返回新建的key對應的value值。
諸位覺得執行結果是否會符合預期?
- 執行結果
將上述程式分別在OS X平臺(STL: MIT)和redhat平臺(STL: SGI)上執行,執行結果如下:
OS X:
$ ./lady
coustruct Lady ... : 0x7fff55e61a30
construct PetHouse ... : 0x7fca71403170
adopt a cat, name: cat0
adopt a cat, name: cat1
construct PetHouse ... : 0x7fca71403200
adopt a dog, name: dog0
adopt a dog, name: dog1
destruct Lady ... : 0x7fff55e61a30
destruct PetHouse ... : 0x7fca71403200
destruct PetHouse ... : 0x7fca71403170
RedHat:
$./lady
coustruct Lady ... : 0x7fff7884b0d0
construct PetHouse ... : 0x606040
destruct PetHouse ... : 0x606040
destruct PetHouse ... : 0x606040
*** glibc detected *** ./lady: double free or corruption (fasttop): 0x0000000000606040 ***
Aborted (core dumped)
- 結果分析
可以看出,在MIT的STL下,上述程式碼執行正常符合預期;在SGI版的STL下,程式core掉!
首先分析SGI版map的core掉原因。
由輸出可見,PetHouse呼叫了一次建構函式,呼叫了兩次解構函式。這是為何?
對Pet類加入拷貝建構函式改造如下:
class Pet
{
public:
Pet()
{
//mPetHouse = new PetHouse;
mPetHouse = NULL;
cout << "construct Pet ... : this" << this << endl;
}
~Pet()
{
if (NULL != mPetHouse)
{
delete mPetHouse;
mPetHouse = NULL;
}
cout << "destruct Pet ... : this" << this << endl;
}
Pet(const Pet &pet)
{
mPetHouse = NULL;
cout << "copy-construct Pet ... : this=" << this
<< "src-pet=" << &pet << endl;
}
...
};
再次執行,結果如下:
$./lady
coustruct Lady ... : 0x7fff53003390
construct Pet ... : this=0x7fff530032d0
copy-construct Pet ... : this=0x7fff53003298, src-pet=0x7fff530032d0
copy-construct Pet ... : this=0x605068, src-pet=0x7fff53003298
destruct Pet ... : this=0x7fff53003298
destruct Pet ... : this=0x7fff530032d0
adopt a cat, name: cat0
adopt a cat, name: cat1
construct Pet ... : this=0x7fff530032d0
copy-construct Pet ... : this=0x7fff53003298, src-pet=0x7fff530032d0
copy-construct Pet ... : this=0x605198, src-pet=0x7fff53003298
destruct Pet ... : this=0x7fff53003298
destruct Pet ... : this=0x7fff530032d0
adopt a dog, name: dog0
adopt a dog, name: dog1
destruct Lady ... : 0x7fff53003390
destruct Pet ... : this=0x605198
destruct Pet ... : this=0x605068
可見,Pet物件被構造了一次,拷貝構造了兩次,並且由於之前Pet物件並未自定義拷貝建構函式,故程式執行過程中的兩次拷貝構造動作都是呼叫編譯器預設生成的預設拷貝建構函式,而編譯器生成的拷貝建構函式內只有一個動作,那就是抓住引數物件和自己本身的地址,快速的進行一次位元組拷貝,亦即進行一次淺複製,這樣就生成了三個Pet物件。在之後對Pet進行析構操作時,由於三個Pet物件中包含的mPetHouse指標指向的是同一個物件,這時問題就出現了,對同一個物件怎麼能析構三次呢??所以,在第一次對Pet析構成功之後再次析構時,程式崩潰!!
core的原因清楚了,那麼為什麼呼叫一次[]
運算子,會級聯構造三個Pet物件呢?理論上講只會構造一個Pet物件才對啊?“原始碼之間,真相大白”!
閱讀SGI版STL中map的operator[]
函式實現原始碼可知,在一次呼叫時,有如下兩個關鍵點需要注意:
1. operator[]函式
mapped_type&
operator[](const key_type& __k)
{
// concept requirements
__glibcxx_function_requires(_DefaultConstructibleConcept<mapped_type>)
iterator __i = lower_bound(__k);
// __i->first is greater than or equivalent to __k.
if (__i == end() || key_comp()(__k, (*__i).first))
__i = insert(__i, value_type(__k, mapped_type())); // // // #3
return (*__i).second;
}
閱讀#3
行可知,在未查詢到key時,會呼叫insert函式插入一個新建的value。具體如下:a, mapped_type()會先呼叫value_type建構函式構造一個待map的物件; b, value_type(__K, mapped_type())會呼叫pair建構函式構造一個map中儲存的真正的pair物件,而pair的建構函式非常暴力:
/** Two objects may be passed to a @c pair constructor to be copied. */
pair(const _T1& __a, const _T2& __b): first(__a), second(__b) { }
這樣就直接導致了剛通過value_type()
構造的物件被再次通過second(__b)
拷貝構造一次。
2.
// map member func @by caft
iterator
insert(iterator position, const value_type& __x)
{ return _M_t.insert_unique(position, __x); }
// _Rb_tree member func @by caft
template<typename _Key, typename _Val, typename _KeyOfValue, typename _Compare, typename _Alloc>
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
_M_insert(_Base_ptr __x, _Base_ptr __p, const _Val& __v)
{
bool __insert_left = (__x != 0 || __p == _M_end()
|| _M_impl._M_key_compare(_KeyOfValue()(__v), _S_key(__p)));
_Link_type __z = _M_create_node(__v); // #4
_Rb_tree_insert_and_rebalance(__insert_left, __z, __p, this->_M_impl._M_header);
++_M_impl._M_node_count;
return iterator(__z);
}
經過一番呼叫後,最終會呼叫到Rb_tree的insert函式,而在Rb_tree的每個insert函式中都會有#4
行所示的操作,即create一個葉子節點用以插入到紅黑樹中:
_Link_type
_M_create_node(const value_type& __x)
{
_Link_type __tmp = _M_get_node();
try
{ get_allocator().construct(&__tmp->_M_value_field, __x); } // #5
catch(...)
{
_M_put_node(__tmp);
__throw_exception_again;
}
return __tmp;
}
由#5
行可知,申請到葉子節點空間後,會接著使用__x
的值來初始化(建立)物件,此處即為第二次呼叫拷貝建構函式。至此,真相大白!
那為何MIT版的STL中map只調用了一次建構函式就完成了插入動作呢?
再次閱讀MIT版的map::operator[]
:
typedef _VSTD::__value_type<key_type, mapped_type> __value_type;
typedef __tree<__value_type, __vc, __allocator_type> __base;
typedef unique_ptr<__node, _Dp> __node_holder;
template <class _Key, class _Tp, class _Compare, class _Allocator>
typename map<_Key, _Tp, _Compare, _Allocator>::__node_holder
map<_Key, _Tp, _Compare, _Allocator>::__construct_node_with_key(const key_type& __k)
{
__node_allocator& __na = __tree_.__node_alloc();
__node_holder __h(__node_traits::allocate(__na, 1), _Dp(__na));
__node_traits::construct(__na, _VSTD::addressof(__h->__value_.__cc.first), __k);
__h.get_deleter().__first_constructed = true;
__node_traits::construct(__na, _VSTD::addressof(__h->__value_.__cc.second));
__h.get_deleter().__second_constructed = true;
return _VSTD::move(__h); // explicitly moved for C++03
}
template <class _Key, class _Tp, class _Compare, class _Allocator>
_Tp&
map<_Key, _Tp, _Compare, _Allocator>::operator[](const key_type& __k)
{
__node_base_pointer __parent;
__node_base_pointer& __child = __find_equal_key(__parent, __k);
__node_pointer __r = static_cast<__node_pointer>(__child);
if (__child == nullptr)
{
__node_holder __h = __construct_node_with_key(__k); // // #6
__tree_.__insert_node_at(__parent, __child, static_cast<__node_base_pointer>(__h.get()));
__r = __h.release();
}
return __r->__value_.__cc.second;
}
由#6
行易知,在查詢到key不存在時直接建立了一個新的葉子節點,緊接著直接進行insert操作,因此未有間接的兩次拷貝構造動作產生。
綜上所述,由於兩種版本的map實現的資料結構略有差異,導致相同的程式碼,用不同的map產生了不同的執行結果。
- 總結
由此可見,STL版本的不同可能會產生意想不到的執行結果。為了程式的相容性更強,也為了空間利用率更高更有效的利用map,筆者建議,map中儲存的key,value最好不要出現自定義型別,最好為原生型別,或者是指標型別。