CString In A Nutshell

I've heard several misconceptions about the use of CStrings and thought it would be beneficial to some of you to clear these up. In this document I will describe how CString works and address 3 key misconceptions:
  • Passing CString by value is bad
  • Using CString causes memory fragmentation
  • CString is slow

Inside CString

The CString data type is 32 bits. Passing CString by value is no more bulky than passing an int by value. You can verify this with an assertion ASSERT(sizeof(CString) == 4);


class CString
{
   ...
   LPTSTR m_pchData;   // pointer to ref counted string data
};

This is the "header" structure of every string:


struct CStringData
{
   long nRefs;             // reference count
   int nDataLength;        // length of data
   int nAllocLength;       // length of allocation
   // TCHAR data[nAllocLength+1]
   TCHAR* data()           // TCHAR* to managed data
      { return (TCHAR*)(this+1); } // this+1 == ((void*)this)+12
};

Lets say you create a CString object like this:

CString str("hello");

First CString calls CString::AllocBuffer(5). This actually allocates 5 + 1 + 12 bytes (chunk + EOS + CStringData). nAllocLength will be set to 5 as will nDataLength. You might think that nDataLength should be 18, but since the extra 13 bytes are ALWAYS allocated, it's more efficient for CString to leave off those extra 13. In release builds, your strings are allocated in blocks of 64, 128, 256, or 512, this is where nDataLength comes in handy. In the case of our 5 character string, nDataLength would be 64. Using blocks reduces memory fragmentation and speeds up operations like adding. Reduction of memory fragmentation is achieved by the use of CFixedAlloc. This class never actually frees the memory allocated (until it is destroyed or explicitly told to), but returns free'd blocks to it's "free pool", so no memory fragmentation occurs. CFixedAlloc can be found in the MFC source directory in FIXEDALLOC.H and FIXEDALLOC.CPP if your curious. For strings larger than 512 characters, the memory is allocated and freed the same as in debug builds.

nRefs is set to 1

m_pchData is set like this: m_pchData = pData->data(); pData is the block of memory allocated by AllocBuffer and cast to CStringData. So what we get looks like this:


   1    5    5 h e l l o \0
---- ---- ---- - - - - - -   <-bytes
               ^m_pchData

Of course to free the block of memory, CString cannot free m_pchData, but instead frees (BYTE*)GetData(); GetData() returns ((CStringData*)m_pchData)-1. Remember that it's casting the pointer to a 12-byte structure and subtracting one structure from it (or 12 bytes).

Reference Counting

So how does reference counting help speed things up? Whenever you use the copy constructor or the operator=(const CString& stringSrc), the only thing that happens is this:


m_pchData = stringSrc.m_pchData
GetData()->nRefs++

If m_pchData had been == stringSrc.m_pchData, nothing at all happens.

So this bit of code is very fast:


void foo(CString strPassed)
{
}

CString str("Hello");

foo(str);

No string copy occurs, and no memory is allocated. A 32-bit value is pushed on the stack, that value is set (strPassed.m_pchData = str.m_pchData), and an integer is incremented (strPassed.GetData()->nRefs++). That's only one operation more than passing an int by value where: A 32-bit value is pushed on the stack, and that value is set. Now granted, it's definetly quite a few more assembly instructions, but that's why we have 500Mhz CPUs, so don't sweat cycles. When it comes to user interfaces, there's no reason to sweat CPU cycles, the computer is capable of executing billions of instructions in a time frame perceivable by a human. Obviously if your doing intensive graphics animation or massive quantities of data manipulation you might wanna look at your inner loops and optimize there.

The reason reference counts are kept is so that CString knows that it's "sharing" a string buffer with another CString object. If foo were to modify strPassed, CString would first allocate a new buffer and copy the string into that buffer (setting it's ref count to 1). Of course if foo never modifies strPassed, the allocation and copy never occur.

Empty Strings

An empty or uninitialized string m_pchData is set to _afxPchNil which looks like this:


  -1    0    0 \0 (EOS)
---- ---- ---- - (_afxInitData)
               ^_afxPchNil

Note that a -1 ref count means that the string is "locked" and so modifying and empty string always results in a new allocation.

Epilogue

Anyhow, that's CString in a nutshell. It's really a fun class to dig into. So if you've ever worried about passing CString objects all over the place, remember that your really essentially only passing a pointer around. It's quite efficient and if you have need to manage dynamic structured data, you might even consider this model.

Please note that this information is accurate as of VC++ 6.0. I've heard that not all of this is true for previous versions of MFC, but I have not personally verified this.