Skip to main content
gamedev.net
🔒 Locked

assert() philosophy

Started by dodheim 27 replies 3.7k views
Original Post
dodheim
dodheim
hi! Please consider the following simple class:

class CFoobar
{
	bool Load(const char* lpszName)
	{
		m_pObj = LoadObject(lpszName);
		return m_pObj != NULL;
	}

	void Unload()
	{
		FreeObject(m_pObj);
	}

	void GetOrientation(CVector& p_vect)
	{
		m_pObj->GetOrientation(p_vect);
	}

private:
	CObject* m_pObj;
};


This object is some kind of a wrapper class, LoadObject() and FreeObject() are defined outside of this class and we let's suppose we can't make any assumptions what FreeObject() would do if we passed it a NULL (this is to generalize the problem). Where would you put checks for NULLs and/or asserts? Also, if you put an assert(m_pObj != NULL) somewhere, would you follow it with an if (m_pObj != NULL) {} so it won't crash in release builds? Probably it's not the best class design example, so if you had any suggestions about using asserts and checking for NULLs unrelated to this example you are welcome. :) thx, d0d
ToohrVyk
ToohrVyk
Assert NULL everywhere where possible, and have it cause a major program meltdown, so it cannot be ignored. In release builds, one should hope that the program has been corrected, but if you believe your job was sloppy enough, have Assert throw an exception instead of melting down, and use the exception to shut off the faulty system and restore the program to a working state.

EDIT: I, myself, never assert for NULL pointers. Either the pointer can normally be NULL and there's no reason to assert, or it cannot be NULL and I use a reference instead. The sole exception to this is when re-seating without allowing NULL-ness, which I enforce with specific objects that can be checked at compile-time never to be NULL.

[Edited by - ToohrVyk on August 30, 2006 6:04:42 AM]
Zymph
Zymph

Asserts should be used when you are sure that something shouldnt be in a particular way. It is sort of a guarantee system to make sure things are the way they are supposed to be. If for any reason something is not the way it is supposed to be, stop the whole thing till the problem is fixed.

In your example, before calling FreeObject(), if you are sure that the value of m_pObj should not be null, put an assert. Doing this will let you know easily if m_pObj is null before passing on to FreeObject which will help you when debugging errors.

use if(m_pObj!=NULL){} only if there is a possibilty that m_pObj may be NULL sometimes before FreeObject() is called. Otherwise use assert and like ToohrVyk said, make it throw exception in release mode.
ApochPiQ
ApochPiQ
I'll just register my agreement with the idea that release mode should not disable assertions. In general, I prefer to leave them totally intact; it doesn't really hurt anything, and it provides extremely valuable information for debugging if users are encountering errors in the wild.
Doggan
Doggan
Code Complete by McConnell has some good information on this topic. Specifically, when to assert, when to throw an exception, when to use an if statement to handle a condition, etc.

The general idea has been pretty much explained. assert() is a valuable tool that should be used often in your program (as there is no downside to it in Release) for situations that should NEVER arise. Situations that may arise even in release build can throw exceptions, which can be used to gracefully recover from the exception. Use if/else for 'non-exceptional' cases - you should not assert, because these situations are to be expected occassionally, so they should be handled by your main program flow.

Also of note, Game Programming Gems I has an article about when/how to use assert. A useful technique is to assert(!"Some descriptive string.") for an area of code that will never be reached. Or, of course, assert(condition && "Some descriptive string.").

Hope that helps.

[Edited by - Doggan on August 31, 2006 10:35:49 AM]
Anon Mike
Anon Mike
I've never seen the point of asserting that a pointer isn't NULL except in (extremely rare) cases where you don't want a pointer to be NULL *and* you're not actually going to use it.

If you are going to use it, and it's NULL, you're going to crash anyway. The assert doesn't really buy you anything and makes your app seem all emo when it goes around telling everybody about how it's about to die.

That being said, asserts are a Good Thing. Better to many than to few.
-Mike
Aardvajk
Aardvajk
Quote:
Original post by Anon Mike
I've never seen the point of asserting that a pointer isn't NULL except in (extremely rare) cases where you don't want a pointer to be NULL *and* you're not actually going to use it.

If you are going to use it, and it's NULL, you're going to crash anyway. The assert doesn't really buy you anything and makes your app seem all emo when it goes around telling everybody about how it's about to die.


Surely it's better for assert() to cause your program to terminate, through something like std::abort(), than for your program to crash with a Windows segment fault error when you try to dereference a NULL pointer, even on a release build?

I appreciate that either way reveals a bug but at least with an assert you can provide some potentially useful information if not actually prevent loss of data.

Sorry if I'm misunderstanding what you mean.
ToohrVyk
ToohrVyk
Quote:
Original post by Anon Mike
I've never seen the point of asserting that a pointer isn't NULL except in (extremely rare) cases where you don't want a pointer to be NULL *and* you're not actually going to use it.


I would argue that if you're about to dereference a pointer, you should use an if/else anyway to determine what to do if it's NULL...

dodheim
dodheim
Thank you for your replies, I will check the resources you mentioned. :]
Quote:
Original post by Doggan
A useful technique is to assert(!"Some descriptive string.") for an area of code that will never be reached. Or, of course, assert(condition && !"Some descriptive string.").

For the record, you surely meant assert(condition && "Some descriptive string.") (no exclamation mark) ;]
Anon Mike
Anon Mike
Quote:
Original post by EasilyConfused
Surely it's better for assert() to cause your program to terminate, through something like std::abort(), than for your program to crash with a Windows segment fault error when you try to dereference a NULL pointer, even on a release build?

The usual argument against trying to handle these sorts of problems is that the handlers tend to destroy useful information (e.g. they ruin the stack trace at the point of failure). I don't know if the assert handler would do that or not. Just letting it crash simplifies debugging because you either just drop into the debugger instantly or you get a crash dump at the exact spot of the problem.


Quote:
Original post by ToohrVyk
I would argue that if you're about to dereference a pointer, you should use an if/else anyway to determine what to do if it's NULL

You are missing the point of an assert. An assert is the programmer saying "it is impossible for this condition to be false if the program is correct". Explicitly checking for NULL via if/else if the pointer can never be NULL is pointless. The whole point of the assert is to verify the assumption that it's ok to not use if/else.

If the pointer can be NULL, even though it's not supposed to be, then yes you should use if/else and not use an assert.

An example would be a public API for a library. The docs for the library might clearly state that you're not supposed to pass NULL but somebody is going to do it anyway (there's nothing stopping them). Therefore you should explictely check via if/else. Contrast this to an internal function later on. That function has every right to assume the pointer is not NULL because the public API was supposed to check it. However it's perfectly reasonable for the programmer to add an assertion just to double check. It also then serves as a form of documentation on pre/post conditions of the function.
-Mike
Xai
Xai
There are many not-completly-orthoganol pairs of things I distinguish between that make this problem so hard to nail down once and for all.

I. As mentioned above a few times there is the difference between something being prohibitied by the contract vs. something being in the realm of inputs a user might think is valid.

II. There is the long-time distinction between debug builds and release builds.

III. There is a distinction between a function failing in an acceptable way vs. failing in an unaceptable way.

IV. The is a distinction between trust and verification.

You have to realize that none of these can have absolute answers, or else programs simply wouldn't be reasonable. Absolute trust would yield a completely dynamic run-time enviroment that provided no assistance in debugging problems that creep in. Absolute verification is impossible to code (good luck pre-verifying that the address you are given is actually a valid pointer to the right kind of object that is still in scope and does not accidentally contain left-over data - it just can't be done) - and would usually be so non-performant as to be useless for games or nearly any real-time use. Absolute distinction between release and debug modes would yield programs that run too slow in debug mode due to excessive checks, and then don't help anyone find problems that only show up in the wild (such as when one crazy customer is using the wrong drivers for their graphics card). Trying to make all failures be acceptable is implossible for the same reason that 100% verification cannot be done ... and not trying to handle any errors gracefully is a dead path from a bygone era.

Personally I do this.

In general, assert contract requirements in debug builds. Not all requirements actually get asserted, but add missing asserts as real-world bugs or programmer desire makes them appropriate, and leave them in the product. These asserts are usually debug only, and compile out of release mode. Not all contract requirments are asserted or checked, as all lines of code operate under some manner of assumptions ... programmer prudence is the key to navigating these decisions.

Check for non-prohibited requirements explicitly, and use branching structures to control results just like any other code decision. This allows you to use simple break-points at debug time if there seem to be bugs with the alternate branch code.

Exceptions should usually only be in response to a few things ... things which are explicitly violating the contract (being given an out-of-range value, etc) and things which arise because code you call is not meeting its contract (the network is supposed to already be setup, but has mysteriously disappeared).

Additionally exceptions can be used when the code you are calling is returning values which you cannot be expected to deal with - although these situations of failure should really be one of your contract's details. Some contract details are implicit instead of explicit. One example is all non error handling code normally runs under the "system and stack has sufficient memory" implicit contract. Also a file copying function usually assumes the stream remains valid while being used, and likewise with database or graphics rendering code - except in situations where the likelyhood of failure is high enough to warrent actual corrective action (for historic reasons the internet connection being lost is one of those oxymoron "expected exceptional" situations - although less and less so all the time).
NotAYakk
NotAYakk
I assert if the parameters are outside of what I would expect the function to be given. If I don't expect my function to be passed null pointers, I'll assert it.

I still write robust code -- code that checks some of the situations that are outside of my expected parameter space.

Using references as "this parameter cannot be null" doesn't work all that well. The caller can still take a null pointer and change it to a null reference without any problem *on their side of the fence*. The only use "reference means non-null" is it auto-documents the interface for you -- the callers still have to check that their pointers they want to pass in are non-null.

"Be strict in what you provide, be lenient in what you accept".
Deyja
Deyja
On the other hand, it's pointless to check for null references (Yes, you can, using compiler-specific arcane casting magic, like ' *(unsigned int*)(&reference) == 0 ', if pointers on your system happen to be the same size as unsigned int.)

Don't worry about them! If you see a reference, assume it is valid. The only way to create a null reference is to do it on purpose, by overriding the type system. If you're doing that, you have bigger issues.
NotAYakk
NotAYakk
Quote:
Original post by Deyja
On the other hand, it's pointless to check for null references (Yes, you can, using compiler-specific arcane casting magic, like ' *(unsigned int*)(&reference) == 0 ', if pointers on your system happen to be the same size as unsigned int.)

Don't worry about them! If you see a reference, assume it is valid. The only way to create a null reference is to do it on purpose, by overriding the type system. If you're doing that, you have bigger issues.


void ref_function(char& foo) {  foo = 'a';}void ptr_function( char* foo ) {  ref_function(*foo);}void look_mom_a_null_reference_with_no_type_system_overrides() {  char* null_ptr = 0;  ptr_function(null_ptr);}


In essence, the only way you can assume that there are no null references is if you assume that everyone who makes a reference from a pointer checks for a null pointer.

But, you can equally assume there are no null pointers passed to a function -- checking the nullness of pointers passed to a function is no harder than checking the nullness of pointers turned into references!

Practically, references can be used to provide code-based documentation of preconditions. But they don't protect you from people giving you null pointers any more than calling your variable "no_null_ptrs_allowed".

...

And as an aside, checking for null references is trivial under every system I've programmed on.

void foo(int& maybe_null) {  // "&" gets a pointer to whatever maybe_null refers to   if(&maybe_null == 0) { // checks if maybe_null is a null reference  }}


I suppose a compiler could say "references cannot be null, so I'll optimize that code out".
JohnBolton
JohnBolton
Quote:
Original post by dodheim
Where would you put checks for NULLs and/or asserts?

Use assert to validate all parameters. Use assert to validate all pointers whose value can be NULL. Use assert to validate array access. ...
Quote:
Original post by dodheim
Also, if you put an assert(m_pObj != NULL) somewhere, would you follow it with an if (m_pObj != NULL) {} so it won't crash in release builds?

If you handle the case where m_pObj is NULL, then there is no reason to use assert there, since the situation is handled.
Quote:
Original post by ApochPiQ
I'll just register my agreement with the idea that release mode should not disable assertions. In general, I prefer to leave them totally intact; it doesn't really hurt anything, and it provides extremely valuable information for debugging if users are encountering errors in the wild.

You aren't using assert enough if it doesn't affect the performance of your code.

Also, you know what really annoys me? That dialog box that pops up everytime a program crashes and asks you if you want to send information to Microsoft. I'm sure Microsoft loves it because it makes it easier for them to ship buggy code, but it doesn't help me at all and it just adds insult to injury.
John BoltonLocomotive Games (THQ)Current Project: Destroy All Humans (Wii). IN STORES NOW!
JohnBolton
JohnBolton
Quote:
Original post by NotAYakk

void ref_function(char& foo) {  foo = 'a';}void ptr_function( char* foo ) {  ref_function(*foo);}void look_mom_a_null_reference_with_no_type_system_overrides() {  char* null_ptr = 0;  ptr_function(null_ptr);}

You must put the assert in ptr_function, since ptr_function does the dereference and assumes that foo is not NULL. Putting it in ref_function is not necessary.
John BoltonLocomotive Games (THQ)Current Project: Destroy All Humans (Wii). IN STORES NOW!
ToohrVyk
ToohrVyk
Quote:
Original post by Anon Mike
You are missing the point of an assert. An assert is the programmer saying "it is impossible for this condition to be false if the program is correct". Explicitly checking for NULL via if/else if the pointer can never be NULL is pointless. The whole point of the assert is to verify the assumption that it's ok to not use if/else.


You're missing the point of my remark. If you want a pointer that cannot be NULL, why are you using a pointer? A reference cannot be NULL. There is no such thing as a null reference, that term is not even defined. So no assert required.

Quote:
void ptr_function( char* foo ) {

ref_function(*foo);

}


I'm sorry, sir, but this does not create a 'null reference'. This creates cheese. Yes, it probably creates some delicately flavoured gouda or emmental. Why? Because it's undefined behaviour. Your program is not incorrect, it's simply not a well-formed program.

Assertions expect the rest of the program to be well-formed (what are they going to do if the stack structure is corrupted, anyway?), and they check if it is correct. Adding an assert(foo) to the above function turns your program from non-well-formed into incorrect.

Next time you check for a null reference, don't forget to check that ((true == true) == true) — nothing else protects you from a #define true false!
Smit
Smit
The developers for the game at work (I work in QA) use asserts extenstivly in the debug builds. Everytime a tester causes an assert the game dumps a report and if possible allows you to carry on with the game.

We then file a bug report describing what we were doing, attach the report/log files etc. The asserts and the reports seems to make the developers lives a lot easier as we report at least 80 bugs a day and they are mostly fixed on the next build (a day or two later).

But anyway, asserts *are* very helpful for both the testers and devs, so use the. [smile]
Enigma
Enigma
Although the point has already been made I shall state it again:

There is no such thing as a null reference in C++.

The only way to obtain something that looks like a null reference is to dereference a null pointer, but the very act of dereferencing a null pointer is undefined behaviour, so by the time you get to assigning the result to a reference the rules of C++ have ceased to apply.

You might argue that this is simply semantics, but there are numerous similar issues and you cannot check them all. Writing code is establishing a contract. Part of that contract is the language standard.

There is no such thing as a null reference in C++

Σnigma
Zahlman
Zahlman
Use RAII and be happy (oh, and throw out all that ridiculous Hungarian notation):

class Foobar{	Foobar(const char* name) : obj(LoadObject(name))	{		if (!obj) 		{			throw some_sort_of_exception();		}	}	void Unload()	{		assert(obj);		// If you were a good little boy and didn't give anyone direct		// access to the pointer, then that can only trip if someone		// did a fandango on core and got "lucky"		FreeObject(obj);	}	void SetOrientation(Vector& vect) // surely you meant this, not Get...	{		obj->SetOrientation(vect);	}private:	Object* obj;};


Quote:
Original post by NotAYakk
In essence, the only way you can assume that there are no null references is if you assume that everyone who makes a reference from a pointer checks for a null pointer.

But, you can equally assume there are no null pointers passed to a function -- checking the nullness of pointers passed to a function is no harder than checking the nullness of pointers turned into references!

Practically, references can be used to provide code-based documentation of preconditions. But they don't protect you from people giving you null pointers any more than calling your variable "no_null_ptrs_allowed".


Sure they do. When you write the system with this sort of assumption, you see, you tend to end up with far fewer explicit pointers in the design. Accepting references tends to create a ripple effect. It also makes it a lot easier to reason about your program, because you end up creating references from pointers explicitly in very few places (which are obvious), and implicitly from values (which won't fail) in the majority of them.

void passByPointer(Thing* foo) {  foo->mutate();}void doWonderfulThings() {  Thing x;  passByPointer(&x); // <-- when we debug the program, this adds noise}


Quote:
And as an aside, checking for null references is trivial under every system I've programmed on.


Begone with your undefined behaviour.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.