Mixing Return Codes and Exceptions in C++

1:56:00 PM 0 Comments A+ a-

Classic RTI Connext DDS API uses integer return codes for reporting erroneous conditions. Checking return codes to know whether a function call succeeded or not is a good and well-established programming practice. C++ offers yet another alternative to report erroneous conditions: Exceptions. Exceptions separate normal (error-free) code-path from error-handling code-path, which increases readability. Exceptions allow error conditions to propagate out to the higher-level functions without cluttering the function interfaces all the way up the call hierarchy with superfluous return codes. Therefore, RTI Connext Request/Reply API in C++ usesexceptions to report all the error conditions.
Given that the Request/Reply API is built on top of the classic DDS API, it is not unusual to have an application use both APIs in the same block of code. In fact, the Request/Reply API anticipates this need and provides access to the raw datareader and datawriter if you want to use the low-level functionality not currently supported through the Request/Reply API. Consequently, you are mixing return codes with exceptions.

A Cocktail That Explodes

Combining return codes and exceptions in the same block of code is an explosive combination. With return codes, programmers must be diligent in keeping track of what has been initialized, which must be finalized at the end of the function or when an error-condition arises. Code blocks are littered with if-then-else. Nested conditionals could not be overused. Smart programmers quickly resort to “goto finalize” and you lament in solitude because that one programming rule you never forgot is violated by the best programmer (and it’s not you) in your team: “Never use goto”. What a sacrilege!
Alas, all that seems to work until you have to use functions that may throw. Your delicate house of cards topples in a heartbeat.
house-of-cards
When the function that may throw actually throws, the long tail of cleanup code is never executed. Suddenly “goto finalize” isn’t the smartest idea anymore. (You chuckle!)
C++ offers try/catch to wade your way through the exceptions thrown at you. Try/catch does not make things simpler because code littered with if-then-else and try/catch is even harder to understand. Nested try statements in catch blocks, anyone?
In essence, combining return codes and exceptions for error handing is a recipe for memory leaks, loaned samples not returned, DDS entities not destroyed, unclosed DB/file handles, etc. Fortunately, we can do better.

Walking Away from the Chaos

First, what you need is a consistent error handling strategy. One easy way would be to convert the return codes from older DDS APIs to exceptions. See below for the mapping of return codes to exceptions used in the RTI Connext Request/Reply API. Note that RETCODE_NO_DATA return code is not handled because it is a condition that may occur often. It is arguable if it is should raise an exception at all.
Return Code
Exception
DDS_RETCODE_UNSUPPORTEDconnext::UnsupportedException
DDS_RETCODE_BAD_PARAMETERconnext::BadParameterException
DDS_RETCODE_PRECONDITION_NOT_METconnext::PreconditionNotMetException
DDS_RETCODE_OUT_OF_RESOURCESconnext::OutOfResourcesException
DDS_RETCODE_NOT_ENABLEDconnext::NotEnabledException
DDS_RETCODE_IMMUTABLE_POLICYconnext::ImmutablePolicyException
DDS_RETCODE_INCONSISTENT_POLICYconnext::InconsistentPolicyException
DDS_RETCODE_ALREADY_DELETEDconnext::AlreadyDeletedException
DDS_RETCODE_ILLEGAL_OPERATIONconnext::IllegalOperationException
DDS_RETCODE_TIMEOUTconnext::TimeoutException
DDS_RETCODE_ERRORconnext::RuntimeException
Now you need a systematic way to handle the exceptions. It goes much beyond just addingtry/catch statements. Those are needed to log and take special actions. It should help separate the clean code path from exceptional code path. But what I really want to get at is exception-safety. How to systematically avoid resource leaks—basic exception safety.

Discipline that Pays Off

R. A. I. I. That’s the take home point for you today. Certainly not the sleekest acronym. But hey, nothing is pretty about errors anyways. Here is what it means.
RAII stands for Resource Acquisition Is Initialization. It is a C++ idiom to simplify resource management. The RTI Connext Request/Reply API uses RAII to clean-up the internally allocated resources even in the face of exceptions. For the memory you allocate, RAII means using the std::auto_ptr, boost::shared_ptr, and their cousins. These smart-pointer classes delete memory automatically when the object goes out of scope, peacefully or due to an exception. Those classes, however, do little to handle memory loaned by DDS.
The request/reply API has a functionality designed to address this issue specifically:LoanedSamples<T>. LoanedSamples class manages the lifecycle of data and sample-info sequences. Just like the smart-pointers, it returns the loans to the DDS middleware when the object goes out of scope. It is programmers responsibility to hold on to the LoanedSamples object if the loans are meant to live longer.
There is one peculiar thing about LoanedSamples that deserves a closer look. LoanedSamples does what it does without allocating any dynamic memory. That is, it is not just another reference-counted smart-pointerish thingy. It is, therefore, extremely fast to create and destroy a LoanedSamples object because it does not take a detour to the memory allocator. As there is no reference counter, it cannot keep track of who is sharing the LoanedSamples object. To avoid double-deletion errors, it disables sharing altogether. You cannot make copies of a LoanedSamples object. Nope. You can only move the object from one place to another. After all, it is a loan that it is managing. What does it really mean to share a loan?
So how does LoanedSamples class help when you mix the classic DDS API with exceptions?
Look for move_construct_from_loans in LoanedSamples<T>. This function will repackage anyloaned data sequence and info sequence from a specific datareader into a handy LoanedSamples object. The LoanedSamples object will steal the loan from the sequences. It will also return them automatically when the LoanedSamples object goes out of scope. This will happen even when there are exceptions. Moreover, unlike sequences, the LoanedSample object can be passed to a function and returned from a function very efficiently. It will ensure that there is exactly one copy of the loan and it is destroyed when the object is destroyed.
Like any well-designed resource manager class, LoanedSamples allows taking back control of the loan it is managing. LoanedSamples<T>.release function relinquishes ownership responsibility to a pair of data sequence and info sequence.

The Secret Sauce

In C++ speak, the behavior of LoanedSamples is known as the move-semantics. Contrary to the popular belief, C++11 is not the only place where move-semantics can be found. C++11 is the first language standard that introduced first-class support to write optimal code using move-semantics. However, in C++03, move-semantics can be closely emulated using just library code. And that‘s what LoanedSamples is doing.

Retaining Control

We now move on to destroying DDS entities. There isn’t any shrink-wrapped API to manage their lifecycle automatically, but it is not hard to come up with one at all. boost::shared_ptr provides a way to call a custom deleter in place of delete. Member functions delete_datareader,delete_datawriter functions are perfect examples of “custom deletors”. If boost is a no-no, it is not hard at all to write such functionality by hand. Here is my quick attempt to manage the lifecycle of a datareader.
template <class T>
class ScopedEntity
{
protected:
  ScopedEntity(T * entity) : _entity(entity)
  { }
public:
  T * get() { return _entity; }
  T * release ()
  {
    T * temp = get();
    _entity = NULL;
    return temp;
  }
private:
  T * _entity;
};
class ScopedDataReader : public ScopedEntity<DDSDataReader>
{
public:
  ScopedDataReader(DDSDataReader * dr)
  : ScopedEntity<DDSDataReader>(dr)
  { }
  ~ScopedDataReader()
  {
    if (get() != NULL) {
    get()->get_subscriber()->delete_datareader(get());
  }
}
private:
  ScopedDataReader(const ScopedDataReader& other);
  ScopedDataReader & operator = (const ScopedDataReader& other);
};
The code snippet above defines a C++ template called ScopedEntity for any DDS entity that is managed automatically. The ScopedDataReader class uses ScopedEntity to manage the lifecycle of a DataReader. The destructor of ScopedDataReader invokes the cleanup code when the DataReader is no longer required. Finally, the copy-construction and copy-assignment operations of ScopedDataReader class are private, which prevents the creation of multiple ScopedDataReader objects managing (and destroying) the same DataReader.
In summary, the RAII idiom deals with resource management very systematically in C++. RTI Connext Request/Reply API in C++ uses RAII to simplify programming. Using exceptions combined with RAII is a powerful approach to write clean, safe, efficient, and future-proof code in C++.