CMap 使用注意说明(English)

来源:互联网 发布:hp8570p windows 7系统 编辑:程序博客网 时间:2024/06/02 13:31

 

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 valuesreturn (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 == LPCTSTRreturn *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==NULLreturn true;} else if ( NULL != *pElement1 && NULL != *pElement2 ) {// both are not NULLreturn **pElement1 == **pElement2;} else {// either one is NULLreturn 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.

这是另外一篇文章,一并转载于此:

 

CMap详解

如何声明CMap
许多人对Cmap的声明模式CMap<KEY,ARG_KEY,VALUE,ARG_VALUE>感到迷惑,为什么不用CMap<KEY,VALUE>呢?实际上,CMap中的的数据最终会是CPair,而CPair内部是(KEY,VALUE)。因此,CMap其实存储的是KEY,而非ARG_KEY。然而,如果你查看MFC的源代码,几乎CMap所有的内部参数传递都是访问ARG_KEY和ARG_VALUE,因此,使用KEY&来代替ARG_KEY似乎是正确的,除了在这些情况下:
1 应用简单的数据类型,如int ,char用值传递与参数传递没有什么不同
2 如果用CString作为KEY,你应该用LPCTSTR   做ARG_KEY而非CString&,接下来我门会讨论原因。

如何让CMap类为自己工作
好的,就象我前面说过的,CMap是一个哈西表,一个哈西表要有“哈西值“——一个UINT类型,用哈西值作为在哈西表中的序数。如果有更多的相同的关键字,他们会组成一个链表。因此,你应该首先构造哈西函数。CMap类会调用摸板函数HashKey()来构造哈西函数。缺省应用和特别版的LPCSTR和LPCWSTR如下:

// 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 >= 1100
template<> UINT AFXAPI HashKey<LPCWSTR> (LPCWSTR key)
#else
UINT AFXAPI HashKey(LPCWSTR key)
#endif
{
    UINT nHash = 0;
    while (*key)
        nHash = (nHash<<5) + nHash + *key++;
    return nHash;
}// specialized implementation for LPCSTR
#if _MSC_VER >= 1100
template<> UINT AFXAPI HashKey<LPCSTR> (LPCSTR key)
#else
UINT AFXAPI HashKey(LPCSTR key)
#endif
{
    UINT nHash = 0;
    while (*key)
        nHash = (nHash<<5) + nHash + *key++;
    return nHash;
}
正如你所见到的,缺省行为是“假定“关键字是一个指针,并且转变成DWORD类型,这就是为什么会出现“error C2440:’type cast’:cannot convert from ‘ClassXXX’to ‘DWORD_PTR’”如果你不提供一个特别的HashKey()函数给你的类就会出现上述情况。并且由于MFC仅仅提供了特殊的工具LPCSTR和LPCWSTR,却没有提供CStringA或CStringW,如果你想要在CMap中用CString,就必须声明CMap<CString ,LPCSTR….>,OK,现在你知道怎么计算CMap的哈西值了,但是因为一个关键字可能对应多个哈西值,CMap就需要找遍整个链表来找到正确的“摸板”,不仅用同样的“哈西值”。当CMap不匹配时,就会访问CompareElements(),另一个摸板方程。// inside <afxtemp.h>
// noted: when called from CMap,
//        TYPE=KEY, ARG_TYPE=ARG_TYPE
// and note pElement1 is TYPE*, not TYPE
template<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;
}
因此,如果你想在自己的类中用CMap,你不得不重写HashKey()和CompareElements()
结束语
1 CMap是一个哈西表,而STL::map是一个树表,对他们做比较是没有意义的。但是,如果你你要重新找到有序的关键字,你就得使用STL::map
2 HashKey()的设计是高效的。你应该提供一个较少冲突的HashKey(),并且容易计算。你要记注,对于有些类来说,这不容易。
3 当用Cmap(或STL::hash_map),要注意哈西表的大小。

附能用于CString的CMap重写的HashKey()和CompareElements()
using namespace std;
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 this
typedef 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;
 }
}

 

---END---

原创粉丝点击