Should I assert against a NULL pointer argument in my C++ function?

From rdiez's Personal Wiki
Jump to: navigation, search

The real-life scenarios are:

  • The function is part of an interface (a public API) where usage errors can be expected and must be dealt with.
 void func ( const char * ptr )
 {
   if ( ptr == nullptr )
     throw std::runtime_error( "Invalid pointer argument." );
   ...
 }
  • The function is an internal function, and the documentation states that a NULL pointer is not allowed.

    Checking against a NULL pointer here would incur an unnecessary performace overhead. Just use the pointer, it will crash if it is NULL, alerting the developer that something is not right.
 // Note that argument 'ptr' must not be NULL.
 
 void func ( const char * ptr )
 {
   const size_t len = strlen( ptr );
   ...
 }
  • My embedded platform does not have an MMU, or my embedded Operating System does not support it, and a NULL-pointer access would go undetected.

    Most microcontroller manufacturers have not realised yet that marking the first and the last 4 KB of memory space as invalid (making them raise traps) would help catch many such C/C++ bugs much earlier. Even when there is an MMU, most embededed OSes do not even try to help here. In this scenario, instead of checking against exactly NULL, you should write some IsValidDataPtr() function that verifies whether the pointer is within a valid range. For example:
 // Note that argument 'ptr' must not be NULL.
 
 void func ( const char * ptr )
 {
   assert( IsValidDataPtr( ptr ) );  // Only really needed in unhelpful embedded environments.
   
   const size_t len = strlen( ptr );
   ...
 }
If you do need to access some near-zero address on your architecture, or you need function pointers, it is probably better to add other such routines like IsValidCodePtr() and IsValidSystemPtr().
  • The pointer argument must not be NULL, but depending on other arguments, it is not always used.
 // Note that argument 'ptr' must not be NULL.
 
 void func ( const bool use_ptr, ..., const char * ptr )
 {
   assert( ptr != nullptr );  // Even if not used, it must not be NULL.
   
   if ( use_ptr )
   {
     const size_t len = strlen( ptr );
     ...
   }
 }
  • The combination of an assertion and proper NULL-pointer error handling is nasty.

    Consider this function:
 // No mention whether ptr may be NULL here.
 
 void func ( ..., const char * ptr, ... )
 {
   assert( ptr != nullptr );
   
   if ( ptr == nullptr )
     throw std::runtime_error( "Invalid pointer argument." );
   ...
 }
Imagine that, after a few years of blissful forgetfulness, you now need to modify the function above. You don't know anything about the surrounding code, and the caller may be 30 levels deep in some obscure library. And now you are faced with the unpleasant question of the day: is the pointer allowed to be NULL not? Your thoughts will probably go like this:
  • Well, the assertion hasn't triggered yet, so it is probably not allowed.
  • How long has the assertion being in place anyway? I could check in the version control history whether it is very old, and thus very likely that NULL pointers have never been allowed.
  • But can you be sure? I fear I'll break something if I remove the error handling.
  • I could also check all calling paths. But it is so complicated, and I do not have the time now.
  • The matter is not actually documented. There may be other users of this library. Maybe they have always used a release build (without the assertions disabled) and have not noticed yet, so the error handling needs to stay.
  • Mmmm, I think I'll leave the assert() in place too. Removing it would mean allowing the pointer to be NULL, but that may not have been the original developer's intention. Maybe the error handling does not really work, as error paths do not get tested often, and the pointer has probably never been allowed to be NULL anyway.
Say you need to split the function or to add more code to it. You'll probably end up with both an assertion and NULL error handling in all new code paths. The result is:
  • If the pointer is never NULL, because it has never been allowed, then you will waste development time (unnecessary error handling code) and execution time (unnecessary pointer checking).
  • If the pointer can be NULL, you'll have unnecessary assert() calls all over the place, which you may have to remove at some point in time. When that happens later on, you will not trust the error handling code any more, so you'll have to test it again.
On a few occasions, I have actually taken the time to investigate such a NULL-pointer situation, to find that it had never made sense to pass a NULL pointer at top-level, and all the error-handling logic below it could be removed, saving both executation time and code space while reducing code complexity.
If you need both error handling and an assertion, state the reason why, and write the code like this:
 void func ( ..., const char * ptr, ... )
 {
   if ( ptr == nullptr )
   {
     assert( false );  // This error needs to be handled, but still, it probably should not happen in our context.
     throw std::runtime_error( "Invalid pointer argument." );
   }
   ...
 }

nullptr vs NULL

Note that, if you are writing new code, you should use the relatively new 'nullptr' keyword instead of NULL.

If your compiler does not support it, you can define it like this:

 #include <stddef.h>   // For NULL.
 #define nullptr NULL  // For my old compiler, which does not support the nullptr keyword yet.