- 问题描述
最近在项目开发中使用到了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最好不要出现自定义类型,最好为原生类型,或者是指针类型。