Introduction
It's not uncommon to see postings on the C++ message board asking for programming help where the problem is stated as being an unwanted assert error. Usually
it's a request for help to get rid of the assert error but it's almost invariably framed as though the assert itself is evil.
I can quite understand the dismay that an assert error can cause to the new programmer. Here's your program doing, mostly, what you want, and then bang! an
assert.
So let's examine asserts and why they're there and what we can learn from them. I should stress that this article discusses how MFC handles asserts.
Linguistics
From a google search ('define assert') and clicking on the web definition.
The verb "assert" has 4 senses in WordNet.
1. assert, asseverate, maintain -- (state categorically)
2. affirm, verify, assert, avow, aver, swan, swear -- (to declare or affirm solemnly
and formally as true; "Before
God I swear I am innocent")
3. assert, put forward -- (insist on having one's opinions and rights recognized;
"Women should assert themselves more!")
4. insist, assert -- (assert to be true; "The letter asserts a free society")
All of the above meanings apply in this instance but meaning 4 is the most literally accurate in the context of asserts. An assert states that the condition
being asserted
is true. If it isn't true the program is in serious trouble and you, the programmer, should be warned of this.
What assert means in code
When a programmer writes an assert statement he's saying, 'this condition must be true or we have an error'. Suppose you're writing a function
that expects to recieve a string pointer.
void CMyClass::MyFunc(LPCTSTR szStringPtr)
{
if (*szStringPtr == '7')
DoSomething();
}
The function reads the memory the pointer points to so it had better be pointing at valid memory. Otherwise you're going to crash! One of the many things
that can go wrong when calling the function is if you were to pass it a
NULL
pointer. Now if it happens that the pointer is in fact
NULL
and your program crashes you don't have a lot of information to work with. Just a message box with a code address and the information that you tried to read
memory at location 0x00000000. Relating the code address to the actual line in your code isn't a trivial task, especially if you're new to the game.
So let's rewrite the function a little.
void CMyClass::MyFunc(LPCTSTR szStringPtr)
{
ASSERT(szStringPtr);
if (*szStringPtr == '7')
DoSomething();
}
What this does is test
szStringPtr
. If it's
NULL
it crashes right away. Hmmm... it crashes? Yes, that's right. But it crashes in a controlled
way if you're running a debug build of your program. MFC has some built-in plumbing to take a controlled crash and connect it to a copy of the debugger. If you were
running a debug build of this program and the assert failed you'll see a messagebox similar to this one.
This shows you which file and which line triggered the assertion. You can abort, which stops the program, or you can try to ignore the error, which sometimes works, or you
can retry, which invokes the debugger and takes you right to the line that failed. This works even if you're running the program stand-alone provided there's a debugger
installed on the computer.
Differences between release and debug builds
I used the phrase 'debug build' quite a few times in the preceding paragraphs. It's important to understand that asserts are a
debug helper. If you're building
a debug version of your program the compiler includes all the code inside the
ASSERT(...)
. If you're building a release version the
ASSERT
itself
and all the code inside the parentheses disappears. The assumption is that you've tested your debug builds and caught all likely errors. If you're
unlucky and missed an error and release a buggy version of your program the hope is that it'll limp along even though it failed a test that an assert would have caught.
Sometimes optimism is a good thing!
It might happen that you want, in a debug build, to assert that something is true and the something you're asserting is code that must be compiled into your program
whether it's a debug build or a release build. That's where the VERIFY(...)
macro comes to the rescue. VERIFY
in a debug build includes
the extra plumbing MFC provides to let you jump into the debugger in the event that the condition isn't satisfied. In a release build the extra plumbing is omitted
from your program but the code inside the VERIFY(...)
statement is still included in your executable. For example.
VERIFY(MoveFile(szOriginalFilename, szNewFileName));
will cause a debug assert if, for whatever reason, the
MoveFile()
function fails in debug builds. Regardless of what kind of build the
MoveFile()
call will be included in your program. But in a release build the failure of the call is simply ignored. Contrast this with
ASSERT(MoveFile(szOriginalFilename, szNewFileName));
In debug builds the
MoveFile()
will be compiled and executed. In release builds the line completely disappears and no file move is attempted. This can lead
to some puzzled head-scratching.
Tracking down asserts in MFC
Now if you're reading this in the hopes of understanding assert errors the odds are that the assert you're debugging didn't come from your code. If you're lucky it came
from MFC, for which you have the source code.
The first thing to remember is that it's not very likely to be caused by a bug in MFC. I don't deny the existence of bugs in MFC but in the dozen years I've been
using MFC I've never had an ASSERT happen due to a bug in MFC.
The second thing to remember is that the ASSERT is there for a reason. You need to examine the line of code that triggered the assert, and understand what it was
testing.
For example, quite a few classes in MFC are wrappers around Windows controls. In many cases the wrapper function simplifies your code by converting a
SendMessage
call into something that looks like a function. For example, the CTreeCtrl::SortChildren()
function takes a handle to a
tree item and sorts the children of that handle within the control. In your code it might look like this.
m_myTreeCtrl.SortChildren(hMyNode);
That's what you write. Internally the class really sends a message to the tree control. You get to call a nice easy function based interface and MFC takes care of
moving the parameters around in the way the message requires. Here's the reformatted function from MFC source code.
_AFXCMN_INLINE BOOL CTreeCtrl::SortChildren(HTREEITEM hItem)
{
ASSERT(::IsWindow(m_hWnd));
return (BOOL)::SendMessage(m_hWnd, TVM_SORTCHILDREN, 0, (LPARAM)hItem);
}
The first thing it does is assert that the underlying window handle in your
CTreeCtrl
object is a valid window! Now I really don't know if bad things
will happen to your system if you try and send the
TVM_SORTCHILDREN
message to a non-existent window. What I do know is that I want to be told if I'm
trying to do it! The assert here alerts me immediately if I'm doing something that has no chance of succeeding.
So if you were calling such a function and got an assert error you'd look at the failed line and see that it's asserting that the window handle is an existing
window. That's the only thing it's asserting. If it fails the only thing that can be wrong is that the window with that handle doesn't exist. That's your clue
to tracking down the bug. From there you would look at the function calling this one and try to determine why the window that you thought existed no longer exists.
So that's a very very brief overview of how MFC uses asserts and how you go about determing why MFC asserted in your project. Now let's look at how we can use
assert in our own code.
void CMyClass::MyFunc(LPCTSTR szStringPtr) revisited
Earlier I alluded to a simple check which simply asserts that the pointer passed to a function isn't
NULL
. We can actually do a good deal better than
that. Both MFC and Windows itself provide us with a bunch of functions we can use to determine if a pointer is pointing at valid memory. To refresh your memory here's
the original function again, and our first draft at improving it.
void CMyClass::MyFunc(LPCTSTR szStringPtr)
{
if (*szStringPtr == '7')
DoSomething();
}
and the first draft of improvements...
void CMyClass::MyFunc(LPCTSTR szStringPtr)
{
ASSERT(szStringPtr);
if (*szStringPtr == '7')
DoSomething();
}
This will alert you to just one kind of error, passing a
NULL
pointer. It'd be nice if we could also test that the pointer is pointing at valid memory
instead of just being a non NULL garbage value. We can do it like this.
void CMyClass::MyFunc(LPCTSTR szStringPtr)
{
ASSERT(szStringPtr);
ASSERT(AfxIsValidString(szStringPtr));
if (*szStringPtr == '7')
DoSomething();
}
This adds a check that
szStringPtr
is pointing at valid memory. The test checks that you have read access to the memory and that the memory includes the
string terminator. A related function is
AfxIsValidAddress
that lets you check if you have access to a memory block of a particular size as specified
in the call. You can also check if you have read access to the block, or if you have write access to it.
Other kinds of ASSERT checks
In addition to checks that use Windows system calls such as
IsWindow()
and memory validation checks it's possible to assert that an object passed to a
function is of a particular type. If you're writing a program that deals with both
CEmployee
objects and
CProduct
objects it's not terribly
likely that those objects will be interchangeable. So it makes sense to verify that functions which work on
CEmployee
objects are passed only those types
of objects. In MFC you do it like this.
void CMyClass::AnotherFunc(CEmployee *pObj)
{
ASSERT(pObj);
ASSERT_KINDOF(CEmployee, pObj);
}
As before, we first assert that the pointer isn't NULL. Then we assert that it's an object pointer of type
CEmployee
. You can only do this with classes
that are derived from
CObject
and you need to add some runtime support. Fortunately the runtime support is really trivial.
You must have declared the object as being at least dynamic. Let me explain. In MFC you can declare that a class contains runtime class information. You do this by
including the DECLARE_DYNAMIC(ClassName)
macro in the class declaration and including the IMPLEMENT_DYNAMIC(ClassName, BaseClassName)
somewhere in the implementation.
The class definition.
class CMyReallyTrivialClass : public CObject
{
DECLARE_DYNAMIC(CMyReallyTrivialClass)
public:
};
and the implementation file
IMPLEMENT_DYNAMIC(CMyReallyTrivialClass, CObject);
.
.
.
If all you want to do is use the
ASSERT_KINDOF
macro those two lines are all that's needed. Now, when you write your program, you use the
ASSERT_KINDOF
macro anywhere that an object pointer is passed to you and it will be tested to see if it does indeed point to an object of that kind.
If it doesn't your program crashes in the controlled way mentioned before and up comes the debugger pointing at the failed assertion. From there... well we've already
covered that ground.
If your object already contains the DECLARE_DYNCREATE
macro or the DECLARE_SERIAL
macro you don't need to use DECLARE_DYNAMIC
because both those macros include the runtime class information required by ASSERT_KINDOF
.
Conclusion
I've tried to illustrate how assert can be used to catch runtime errors and drop you into the debugger at the line of code that caused the assert. We looked at how
you can work backwards from the assert to determing the reason the assert failed. Along the way we briefly looked at how you can test in your own software the
validity of pointers to memory and how you can check that you were passed a pointer to an object of the kind your code is expecting.
In recent years I've been using (perhaps overusing) asserts as runtime checks in my code. It's become automatic to pepper my code with assertions that pointers
are valid and point to the kind of object my code is expecting. I find it pays off. It's a rare occurence these days that I have to cope with a crash caused by a NULL
pointer or a pointer to the wrong kind of object.
History
12 Mar, 2004 - Initial version