Sanity Checking
Basically, the Object Registration functions extend the member access functions previously described by inserting sanity checking code into each of these member access functions, as well as the object's destructor. Code that sets and clears the object's sanity is also inserted into the object's constructors and destructor, respectively, thereby covering all access points to the object, as shown in Listing 4.
// NON-TRIVIAL CONSTRUCTOR HPISocket_c::HPISocket_c(string const& User,string const& Password) throw(EXSanity_c) { // CODE OMMITTED FOR BREVITY SetSanity(); // THIS OBJECT IS NOW AVAILABLE FOR USE } // MUTATOR String HPISocket_c::DBUser(String const& x) throw(EXSanity_c) { SanityCheck(__LINE__,__FILE__); // VALIDATE OBJECT INTEGRITY if(x.length()) m_DBUser = x; return m_DBUser; } // ACCESSOR String HPISocket_c::DBUser() const throw(EXSanity_c) { SanityCheck(__LINE__,__FILE__); // VALIDATE OBJECT INTEGRITY return m_DBUser; } //DESTRUCTOR HPISocket_c::~HPISocket_c() throw(EXSanity_c) { SanityCheck(__LINE__,_FILE__); // VALIDATE OBJECT INTEGRITY // CODE OMMITTED FOR BREVITY ClearSanity(); // THIS OBJECT IS NOW UNUSABLE }
This sanity-checking code is responsible for ensuring object integrity and is designed to deal with situations where a consuming object attempts to use an object that is either improperly constructed, incorrectly defined, or has deemed itself unusable for whatever reason. The mechanics of these functions entail very low overhead in that they contain very little code and are fully inlined. Once implemented, there is no way to use an invalid object, as the object will throw a well-formed exception whenever a consuming object tries to invoke its services or use its data.
Implementation of the pattern, detailed in Listings 2 and 3, is pretty straightforward. Let's start with the class declaration. First and foremost, the functions and data required for this pattern are declared private, as previously discussed. It should be the sole discretion of the target object to decide A) when sanity is gained or lost and B) when to check its sanity. Thus, I strongly suggest not making them public. When inheritance is an issue, simply declare these functions protected so child classes need not reinvent the wheel.
OK, now let's visit the actual declarations. First, a static member variable of type long is defined. This variable is used to contain a reference value that all subsequent instances of the class will use. By this I mean that this value, once assigned, will be used to check all instances of this class to determine the sanity of the instance. I usually name this variable sm_clSerialNumber. The sole purpose of this variable is to serve as the yardstick against which all object instances are compared. The value of this member is assigned either inline, or in file scope, depending on the compiler being used. The value assigned is not important, as long as the pattern has a very low probability of being found in random memory. I usually use a large hex number, like 0x001001001.
Next, a mutable member variable is defined, also of type long, which I usually name m_mlSerialNumber. This member variable is used to contain the current registration value. Next, three member functions are declared, each of which is a constant function returning nothing. These functions must be declared constant in order for them to be callable from const member functions of the class. This is also why the local member variable was declared mutable. The three functions are implemented in listing 4, and work as follows:
SetSanity is used to set the local registration variable m_mlSerialNumber to the value contained in sm_clSerialNumber. Once this has been accomplished, the object is deemed to be valid. This function is usually invoked in the constructor, once the function has deemed itself instantiable. Note that returning from the constructor prior to calling this function will leave the object unreachable!
ClearSanity is used to reset the local m_mlSerialNumber to a value other than that found in sm_clSerialNumber. Usually, this value is zero, which I find works best. However, this variable can be set to any arbitrary value that differs from sm_clSerialNumber as needs dictate. This function is usually invoked as the last executable line of the class' destructor. Once this function is invoked, the object is deemed invalid. Subsequent use of the object will throw a well-formed exception.
SanityCheck is used to determine whether the object is valid or not. This function will first check to see if the this pointer of the instance is NULL. If so, the object is being invoked through a null pointer dereference, which, if not dealt with, will cause and access violation (or Segment Fault in Unix variants) that is usually not recoverable. There are mechanisms to deal with these OS faults that are beyond the scope of this article. Next, this function will check to see of the local registration variable is the same as the class wide registration variable. If either condition fails, the function will throw a well-formed exception.
The foregoing is the heart of the pattern and thus deserves a bit more explanation. In C++, the code segment and the data segment are separate. When the code segment is joined with a data segment, we have an object instance. This this pointer in the object points to the data segment the code segment refers to, whether implicitly or explicitly. When this data segment pointer is NULL, or contains an invalid data segment address, the this pointer is invalid and any operation or dereference of it will cause a segment fault. This is exactly what we're looking to be proactive with in the SanityCheck function. If the this pointer is not null, we next check the serial number of the object against the static serial number that is maintained in the code segment, thus immutable. If the values don't match, either we have a garbage pointer or the object was "turned off" by ClearSanity. In either case, the object is unusable and SanityCheck will throw a well-formed exception. A full explanation of how this pointers are bound can be found in the ANSI C++ Standard.
The SanityCheck function is called by every other function in the object, as shown in Listing 5. This will also include the member access functions described earlier as well as the destructor, but exclude the constructors for obvious reasons. By including calls to this function at the top of each member function of the object SanityCheck becomes, in essence, a gatekeeper for the class. Every time a consuming objects attempt to use an invalid object the result is a well-formed exception.
inline DWORD const HPISocket_c::TimeLastUsed() const throw(EXSanity_c) { SanityCheck(__LINE__,__FILE__); return m_tTimeLastUsed; } inline char const HPISocket_c::DataClass(char x) throw(EXSanity_c) { SanityCheck(__LINE__,__FILE__); return m_cDataClass = x; }
This pattern, then, effectively deals with object-level memory allocation issues such as null pointers, uninitialized pointers, and pointers or references to objects that have been destroyed.
When calling code tries to dereference a class, a well formed exception is thrown. If this exception is not caught, the exception, its message and stack trace will be available in the dump log or, if my exception class hierarchy is used, in the system log (for Solaris) or the Event log (for Windows). Or, with a proper try/catch pair, the exception can be caught, and the application can attempt to recover. Either way, it becomes almost trivial to find and correct these bugs, now that we have a way to find where and when they occurred.
In summary, then, the OVR pattern and the functions that implement them effectively bulletproof classes from unintentional misuse due to simple coding bugs, and provide a simple means to track them down. But in order to truly bulletproof a class, the issue of logic bugs and business rule violations must also be dealt with. That is the subject of a different article.