![]() |
Languages »
C / C++ Language »
General
Beginner
License: The Code Project Open License (CPOL)
Assert is your friendBy Rob MandersonHow to use assert to find bugs in your programs |
VC6, VC7, VC7.1Win2K, WinXP, Win2003, Visual Studio, MFC, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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.
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.
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.

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.
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.
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.
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); // Our old friend, it can't be a NULL
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:
// Various class members and functions...
};
and the implementation file
IMPLEMENT_DYNAMIC(CMyReallyTrivialClass, CObject);
.
.
.
// Other class functions...
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.
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.
12 Mar, 2004 - Initial version
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 11 Mar 2004 Editor: Rob Manderson |
Copyright 2004 by Rob Manderson Everything else Copyright © CodeProject, 1999-2009 Web21 | Advertise on the Code Project |