C++ Zen of the Sad Path

Epitome of a C++ exception. Jester Box.

No, no, no, this is not an article about C++ being on the sad path, I just simply would like to show and explain clearly the key paradigms in regards to error handling. But why not just learning from the official WG21 (consensus) recommendation?

Because there is no such thing.

Standard C++ has no standard error handling

C++ Illuminati? Stack Overflow, 200k+ status carriers? They will never tell you why is this or that error handling the one you should follow. WG21 and succinctly named "subcommittees"? Nope.

Ok, let us then "just" use std lib, as advised. But, std lib has them by dozens, error handling idioms that is. How and when any of them should be used? Which one to choose and use? There is no official advice officially underwritten. <system_error>? std::exception? Wait, maybe std::errc? Follow the <filesystem> error handling paradigm? Is std::expect official? Should we wait for what Herb seems to be pushing as std::error? Bjarne seems decisive not in favor.

What could possibly go wrong? ™

Everything you did not want to know about error handling emanates from here. Let's begin small. Here is our simple use case, we shall use:

char * buffer_allocate ( size_t size_ )
{
   // allocate and return native char pointer 
   // to the dynamicaly allocated block of memory
   // of given size   
}

Small but what a devious, divisive, short piece of C++ code that is. I might even call it "Jester in a Box".

Fine. If that is C++, why not using standard C++ new then? Well alow me to not want std::bad_alloc to surprise us. We want total control of our destiny if and when something goes wrong, that is when there is no enough heap to allocate and return a pointer to that allocated, nicely cleaned contiguous block of memory.

All the possible outcomes caused by that function might be joyously categorized, joyously for this bitter pill to swallow easier. Let us form some kind of scale in regards to the state of mind of the developer or even decision-makers on that project:

State of mind v.s. error handling paradigms

  • Delirious: Nothing will ever go wrong!
  • Optimistic: Most of the time all will be fine?
  • Pragmatic: Let us prepare for no memory event.
  • Pessimistic: Most of the time there will be no enough memory.
  • Dark: When there is no memory there is no reason to live.

"That is not enough, where is the main ?!" Jumps in the clever onlooker, readily. Well dear member of the audience, the main() reveals the state of mind.

Delirious: Nothing will ever go wrong!

char * buffer_allocate ( size_t size_ )
{
    return  static_cast<char *>( calloc( size_, sizeof(char) ) ) ;
}
int main( int argc, char ** argv )
{
   char * ptr_ = buffer_allocate( str_to_size_t_(argv[0] ) );
   // nah, no need for this:   free( ptr_ ) ;
   return EXIT_SUCCESS ;
}

Ok, there is no even free() up there. Why should there be, some people might (and do) ask. And they proceed: OS will clear that up, after process exits. No need to worry. I might call that: Happy go lucky software design. Always happy and always lucky software designers method. But is that so outlandish as you are led to believe? Is this a real disaster in the form of C++ code?

Well on your one-man-band "C++naut" desktop that is certainly a moot point, that might be plain wrong or interesting. But definitely not so much inside some (god forbid) pace-maker. Or (god forbid) a large passenger airplane system. Catch my drift? As long as there is no requirement you are "free not to free", the memory you have asked run time to allocate that is. And as long as your program is not endangering people and livestock well being. Just please keep it on your machine in any case

Using that little program, might even be a good (sobering) exercise. Execute the above program from some script your OS supports, in one very long loop. And than see what happens. You might even report your findings in some "Happy go lucky" blog.

You will learn every executable runs in some kind of run-time environment. Run-time environments are sometimes very forgiving and sometimes are not. And writing software for a living is a situation where what you write will be executing mostly in the second group.

Optimistic: Most of the time all will be fine!

char * buffer_allocate ( size_t size_ )
{
   return  static_cast<char *>( calloc( size_, sizeof(char) ) ) ;
}
// written by an optimist
 str_to_size_t_( const char * ) ;
int main( int argc, char ** argv )
{
   char * ptr_ = buffer_allocate( str_to_size_t_(argv[0] ) );
   free( ptr_ ) ;
   return EXIT_SUCCESS ;
}

That was not written by a delirious person but there are simply no error checks whatsoever up there. The optimistic developer simply does most of the things right but never checks for errors. The key assumption is here: "most of the time all will be fine". For example: Most of the time there will be enough memory.

I (in turn) might assume some serious people who can show empirical results from some very deep and long tests, statistically proving that "most of the time" might be some surprisingly large percentage. Thus there you have it: the case for optimistic no-error-checking software.

Although I can not imagine why anyone would use or buy such software?

It very well might be people simply instinctively write the code which does not allocate a lot of memory in one go. Still that is a very big speculation. Even if using std lib constructs one makes the optimistic version more resilient:

// no need to free
std::string str_ = buffer_allocate( str_to_size_t_(argv[0] ) );

Will that work? Same as the delirious version it will work. Until it crashes. True, that is a bit more responsible code but certainly a pretty reckless one in the same time. No mission-critical component or system can be written in this "optimistic" state of mind. Imagine e.g. Linux kernel designers assuming there will be enough memory "most of the time". The Kernel will be crashing in a completely unpredictable fashion. All together with the program, you are using at the moment of the crash, your desktop, your Linux Shell, and even your machine. One should be extremely optimistic to use OS built on top of the optimistic kernel.

Pragmatic: Let us prepare for no memory event.

By believeing in modern C++.

char * buffer_allocate ( size_t size_ )
{
     char * ptr_  =  static_cast<char *>( calloc( size_, sizeof(char) ) ) ;
     // here do check if ptr_ is null_ptr
     if ( ptr_ == null_ptr ) {
     // pragmatist preferred action plan
     // some do call that 'sad path'
     // once on the sad path function never returns
         throw std::bad_alloc() ;
     }
     // otherwise
     // return ptr_ only if it is not null_ptr
     // some do call that 'happy path'
     return ptr_ ;
}
int main( int argc, char ** argv )
{
  try {
   char * ptr_ = buffer_allocate( str_to_size_t_(argv[0] ) );
    // we are on the happy path
         free( ptr_ ) ;
     } catch (const std::bad_alloc& e) {
        std::cout << "Allocation failed: " << e.what() << '\n';
    }
   return EXIT_SUCCESS ;
}

Ok, that seems very sober design. Happy path, sad path, exceptions, and even <iostream> is included. Pragmatist writes C++ in the belief it works and behaves as it is advertised. The pragmatist delivers software using the programming language he is asked to use. No time for going to Sunday masses of the church of the divine "no exception" or attending the self-support groups of "never use std::iostream".

Actually, I am pretty sure, the true pragmatist would do this:

// it does what it says on the tin
char * buffer_allocate ( size_t size_ ) {
     return new char[size_];
}

int main(void) {
  try {
     std::unique_ptr<char> buffy_ { allocate_buffer( str_to_size_t_( argv[0]) } ;
         } catch ( std::bad_alloc & x) {
            // straight from the good book
            std::cerr << "Ther is no enough memory" << std::endl;
         }
    return 42;
}

Human software users of the MS Word’s of today certainly could not care less about "what is standard c++ error handling". You know the kind of people that actually pay for the licenses. Or go to the Apple store and ask what is best for them. I am not diminishing them. People have jobs to do and no time for much else besides their families. That is, I might dare to guess, approx 99% of the users out there. And that includes the developers coding apps written in pragmatic C++. If they happen to be written in C++ that is.

The one issue C++ has here is competition. It is now almost 2021. Perhaps close to 90% of your everyday consumer GUI apps on tablets, phones, and watches are not written in C++. Nor they ever will be. There is a very (very) small number of C++ pragmatists asked to deliver pragmatic approach GUI fronted, written in C++ code. C++ is being moved "down bellow" or should we rather say "left to graze on the pastures on the Cloud"; on millions of the servers of today’s enormous data centers networked in global networks. And over there C++ pragmatism is not the ruling religion.

Pessimistic: Most of the time there will be no enough memory.

This is the territory where you as the C++ coder are on the very bottom of a very vicious "survival-of-the-fittest" chain. Naturally, you are pessimistic about entering that jungle. Hence this Sun Tzu influenced "defensive programming" paradigm:

char * buffer_allocate ( size_t size_ )
{
   char * ptr_ = (char *)very_special_calloc( size_, sizeof(char) ) ;
   if ( ptr_ == nullptr )
   {
       // 1. log the fact there was no enough memory
       log_memory_failed_effort( ptr_ ) ;
       // 2. use a internal infrastructure system to ask in turn 
       //    everyone else to release all the heap they can
       //    and that includes all the processes too on this machine
       //    this might even involve human data centre operators too
        ptr_ = there_must_be_enough_memory_for_the_buffers ( size_ ) ;
       // 3. log the outcome
       log_memory_reclaim_effort( ptr_ ) ;
   }
      return ptr_ ;
}
int main( int argc, char ** argv )
{
   // might take a *lot* of time, but the primary objective has been met
   char * ptr_ = buffer_allocate( str_to_size_t_(argv[0] ) );
   // you better do this
   free( ptr_ ) ;
   return EXIT_SUCCESS ;
}

Primary objective: there must be enough memory always.

And each memory allocation is very likely to fail. My problem with that approach is over-engineered defenses. To develop and test and use effectively (if ever) that kind of software components cluster, surely must take a lot of effort and money.

And yes, in case you have not been pessimistic enough, the data center monitoring system will notify the administrators. And that will teach you to never (ever) mess with the administrators. As they will call you for sure, and you are contractually obliged to answer that call even at 3 AM. Actually, your boss will call you as they will call him, at 3 AM.

Dark: When there is no memory there is no reason to live.

If there is no memory ye shall not return!

//  this might be the C function 
char * buffer_allocate ( size_t size_ )
{
   // note: real man use c style cast
   char * ptr_ = (char *)calloc( size_, sizeof(char) ) ;
     if (! ptr_ ){
     // compiler dependant
     // OS dependant
       __fastfail(7) ;
    }
        return ptr_ ;
}
int main( int argc, char ** argv )
{
   char * ptr_ = buffer_allocate( str_to_size_t_(argv[0] ) );
   free( ptr_ ) ;
   return EXIT_SUCCESS ;
}

That software will crash if there is no enough memory. Contrary to the "Delirious" approach we know where and when exactly it will do that. And there is this "magical seven" symbolic constant that surely has some deep meaning.

In these kinds of apps, anything can deliberately crash. There are systems (e.g. Erlang) where component design philosophy works exactly like that: crash fast and let the run time environment take care of the rest. The obvious question being: what if the run time crashes?

I can (and will) add some more content to this post soon, but for now:

Conclusion: There is nothing wrong with you

Go back to your happy C++ life. Just please whatever the "C++ Illuminati" tell you, take it with a pinch of salt. There is no sad path or right path in error handling. There are only the project requirements. And on that project, you might be the navigator. You are responsible for charting the right path in tune with the requirements. But:

You can not satisfy everyone at the same time

All five personalities will pop-up sooner or later. There is no way to satisfy them all. That is where these "sad path / happy path" ideas are coming from as some explanations have to be developed for whoever can make the decision about the approach you are required to make. You need to know there is no paradigm to cover all the paths and at the same time satisfy all the personalities, in and around the project.

That includes also your peers and that includes not your peers, a.k.a. "decision-makers".

Right path is not the easy one
Right path is not the easy path

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 thoughts on “C++ Zen of the Sad Path”