The Problem
CArray
is one of my favorite classes. It's probably saved me more time than any other code I've used. Because it's such a popular piece of code, I imagined it had been used in every possible situation, and all the kinks had been worked out - so I was surprised when we stumbled onto this problem a few days ago.
Take a look at the following code and see if you can spot the bug:
CArray< int,int&> my_carray;
int some_number = 1;
my_carray.Add(some_number);
for(int i=0; i<=10; i++) {
my_carray.Add(my_carray[0]);
}
TRACE("\nIndex\tValue");
for(int j=0; j<=10; j++) {
TRACE("\n%d\t%d", j, my_carray[j]);
}
The TRACE
output is:
Index Value
0 1
1 -572662307
2 1
3 1
4 1
5 -572662307
6 1
7 1
8 1
9 -572662307
10 1
Probably not what you were expecting.
Stepping into Afxtempl.h
A few snippets of code from Afxtempl.h will help show what's going on under the hood. We'll start by looking at the Add
function:
AFX_INLINE int CArray< TYPE, ARG_TYPE>::Add(ARG_TYPE newElement)
{
int nIndex = m_nSize;
SetAtGrow(nIndex, newElement);
return nIndex;
}
Nothing strange in the Add
, it just calls SetAtGrow
:
template< class TYPE, class ARG_TYPE>
void CArray< TYPE, ARG_TYPE>::SetAtGrow(int nIndex, ARG_TYPE newElement)
{
ASSERT_VALID(this);
ASSERT(nIndex >= 0);
if (nIndex >= m_nSize)
SetSize(nIndex+1, -1);
m_pData[nIndex] = newElement;
}
Notice that SetSize
gets called before the assignment of newElement
when the if
statement is true. Now look at the code for SetSize
: (it's a big function - the interesting part is in bold near the bottom)
template< class TYPE, class ARG_TYPE>
void CArray< TYPE, ARG_TYPE>::SetSize(int nNewSize, int nGrowBy)
{
ASSERT_VALID(this);
ASSERT(nNewSize >= 0);
if (nGrowBy != -1)
m_nGrowBy = nGrowBy;
if (nNewSize == 0)
{
if (m_pData != NULL)
{
DestructElements< TYPE>(m_pData, m_nSize);
delete[] (BYTE*)m_pData;
m_pData = NULL;
}
m_nSize = m_nMaxSize = 0;
}
else if (m_pData == NULL)
{
#ifdef SIZE_T_MAX
ASSERT(nNewSize <= SIZE_T_MAX/sizeof(TYPE));
#endif
m_pData = (TYPE*) new BYTE[nNewSize * sizeof(TYPE)];
ConstructElements< TYPE>(m_pData, nNewSize);
m_nSize = m_nMaxSize = nNewSize;
}
else if (nNewSize <= m_nMaxSize)
{
if (nNewSize > m_nSize)
{
ConstructElements< TYPE>(&m_pData[m_nSize], nNewSize-m_nSize);
}
else if (m_nSize > nNewSize)
{
DestructElements< TYPE>(&m_pData[nNewSize], m_nSize-nNewSize);
}
m_nSize = nNewSize;
}
else
{
int nGrowBy = m_nGrowBy;
if (nGrowBy == 0)
{
nGrowBy = m_nSize / 8;
nGrowBy = (nGrowBy < 4) ? 4 : ((nGrowBy > 1024) ? 1024 : nGrowBy);
}
int nNewMax;
if (nNewSize < m_nMaxSize + nGrowBy)
nNewMax = m_nMaxSize + nGrowBy;
else
nNewMax = nNewSize;
ASSERT(nNewMax >= m_nMaxSize);
#ifdef SIZE_T_MAX
ASSERT(nNewMax <= SIZE_T_MAX/sizeof(TYPE));
#endif
TYPE* pNewData = (TYPE*) new BYTE[nNewMax * sizeof(TYPE)];
memcpy(pNewData, m_pData, m_nSize * sizeof(TYPE));
ASSERT(nNewSize > m_nSize);
ConstructElements< TYPE>(&pNewData[m_nSize], nNewSize-m_nSize);
delete[] (BYTE*)m_pData;
m_pData = pNewData;
m_nSize = nNewSize;
m_nMaxSize = nNewMax;
}
}
What happens is that m_pData
gets deleted in SetSize
, and when it returns to execute the line m_pData[nIndex] = newElement
in SetAtGrow
, newElement
is a reference to the OLD m_pData
that was just deleted!
Required Conditions
The problem only occurs when all three of the following are true:
- The second parameter in the
CArray
template is a reference.
- You call one of the following
CArray
functions and pass an existing array element as the newElement
parameter:
Add
SetAtGrow
InsertAt
- Adding the element in 2) causes a memory allocation in the
SetSize
function.
Given all of these conditions, you're probably thinking this is a bit contrived. Actually it isn't. Although I cooked up the example code shown above, so I could demonstrate the problem, the genuine bug was found by running our application with a file that a customer had sent in because the application was giving incorrect results. We ran our application with BoundsChecker, and it found the CArray
referencing a dangling pointer. Once this code was changed, the application worked properly.
Work-around
There are a number of ways to avoid/fix the problem: