CMap 使用注意說明

Introduction

Programmers like me, learnt STL::map before CMap always think CMap is difficult to use, and always try to use CMap in the way as a STL::map. In this article, I will explain more information about CMap and what should you do to use it for your own custom class. And as the end of this article, I will show an example of how to use CMap correctly with CString* (note I mean CString pointer and not CString :> )

CMap Internal

The first thing to be noted is that CMap is actually a hash map, and not a tree map (and usually a Red-black tree) as STL::map. Below shows the internal structure of a CMap.

 

How to declare a CMap

Many people get confused about CMap's declaration CMap<KEY, ARG_KEY, VALUE, ARG_VALUE>, why not just CMap<KEY, VALUE>

In fact, the ultimate data container in CMap is CPair, and the internal of CPair is {KEY, VALUE}. Therefore, CMap will really store a KEY, and not ARG_KEY. However, if you check with the MFC source code, almost all the internal parameters passing within CMap itself is called with ARG_KEY and ARG_VALUE, therefore, using KEY& as ARG_KEY seems always be a correct answer, except

  1. You are using primitive date types like int, char, where pass-by-value makes no difference (may be even faster) with pass-by-reference
  2. If you use CString as KEY, you should use LPCTSTR as ARG_KEY and not CString&, we will talk more about this later

So what should I do to make CMap works with my ClassX

Well, as I mentioned earlier, CMap is a hash map, a hash map will try to get the "hash value" -- an UINT -- from the key and use that hash value as the index in the hash table (well, actually it is hash value % hash table size). If more then one key have the same hash value, they will be link in a link list. Therefore, the first thing you have to do is to provide a hash function.

CMap will call a templated function HashKey() to do the hashing. The default implementation and specialized version for LPCSTR and LPCWSTR are listed as follows:

// inside <afxtemp.h>template<class ARG_KEY>AFX_INLINE UINT AFXAPI HashKey(ARG_KEY key){	// default identity hash - works for most primitive values	return (DWORD)(((DWORD_PTR)key)>>4);}// inside <strcore.cpp>// specialized implementation for LPCWSTR#if _MSC_VER >= 1100template<> UINT AFXAPI HashKey<LPCWSTR> (LPCWSTR key)#elseUINT AFXAPI HashKey(LPCWSTR key)#endif{	UINT nHash = 0;	while (*key)		nHash = (nHash<<5) + nHash + *key++;	return nHash;}// specialized implementation for LPCSTR#if _MSC_VER >= 1100template<> UINT AFXAPI HashKey<LPCSTR> (LPCSTR key)#elseUINT AFXAPI HashKey(LPCSTR key)#endif{	UINT nHash = 0;	while (*key)		nHash = (nHash<<5) + nHash + *key++;	return nHash;}

As you can see, the default behavior is to "assume" key is a pointer, and convert it to DWORD, and that's why you will get "error C2440: 'type cast': cannot convert from 'ClassXXX' to 'DWORD_PTR'" if you don't provide a specialized HashKey() for your ClassX.

And because MFC only have specialized implementation for the LPCSTR and LPCWSTR, and not for CStringA nor CStringW, this is why if you want to use CString in CMap, you have to declare CMap<CString, LPCTSTR....>

Ok, now you know how CMap calculate the hash value, but since more than one key may have the same hash value, CMap needs to traverse the whole link list to find the one with exactly the same key "content", not only with the same "hash value". And when CMap do the matching, it will call CompareElements(), another templated function.

// inside <afxtemp.h>// noted: when called from CMap, TYPE=KEY, ARG_TYPE=ARG_TYPE// and note pElement1 is TYPE*, not TYPEtemplate<class TYPE, class ARG_TYPE>BOOL AFXAPI CompareElements(const TYPE* pElement1, const ARG_TYPE* pElement2){	ASSERT(AfxIsValidAddress(pElement1, sizeof(TYPE), FALSE));	ASSERT(AfxIsValidAddress(pElement2, sizeof(ARG_TYPE), FALSE));	// for CMap<CString, LPCTSTR...>	// we are comparing CString == LPCTSTR	return *pElement1 == *pElement2;}

Therefore, if you want to use CMap with you own custom ClassX, you will have to provide a specialized implementation for HashKey() and CompareElements().

Example: CMap with CString*

Provided as an example, below is what you need to do to make CMap works with CString*, and of course, using the string content as the key, and not the address of the pointer.

template<> UINT AFXAPI HashKey<CString*> (CString* key){	return (NULL == key) ? 0 : HashKey((LPCTSTR)(*key));}// I don't know why, but CompareElements can't work with CString*// have to define thistypedef CString* LPCString;template<>BOOL AFXAPI CompareElements<LPCString, LPCString> (const LPCString* pElement1, const LPCString* pElement2){	if ( *pElement1 == *pElement2 ) {		// true even if pE1==pE2==NULL		return true;	} else if ( NULL != *pElement1 && NULL != *pElement2 ) {		// both are not NULL		return **pElement1 == **pElement2;	} else {		// either one is NULL		return false;	}}

And the main program is as simple as the following:

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]){	CMap<CString*, CString*, int, int> map;	CString name1 = "Microsoft";	CString name2 = "Microsoft";	map[&name1] = 100;	int x = map[&name2];	printf("%s = %d/n", (LPCTSTR)name1, x);*/	return 0;}--------- console output ---------Microsoft = 100

Please note that the program can compile without error even without the specialized HashKey() and CompareElements(), but of course, the out output will then be 0, probably not what you want.

My final note about CMap

  1. CMap is a hash map and STL::map is a tree map, it is no meaning to compare the two performance (compare apple to orange!). But if you will retrieve the keys in sorted order, then you will have to use STL::map.
  2. The design of HashKey() is critical to the overall performance. You should provide a HashKey() that has low collision rate (different key generally would have different hash value) AND is easy to calculate (not a MD5 of the string, etc..). And we have to note that -- at least for some of the classes -- this is not easy.
  3. When using CMap (as well as STL::hash_map), always beware of the hash table size. As quoted in MSDN "the hash table size should be a prime number. To minimize collisions, the size should be roughly 20 percent larger than the largest anticipated data set". By default, CMap hash table size is 17, which should be ok for around 10 keys. You can change the hash table size with InitHashTable(UINT nHashSize), and only can do so before the first element is added to the map. You can find more prime numbers at here. (And don't mix-up with CMap(UINT nBlockSize), nBlockSize is to acquire more than one CAssoc to speed up the creation of a new node.)

Reference

History

  • 17th March 2006: Initial version uploaded.


Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=649293

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章