Object Registration and Validation

Invalid objects are the root of many a programming evil. Eric shows one way to eliminate them from your code.


June 21, 2007
URL:http://drdobbs.com/cpp/object-registration-and-validation/199905993

Eric Gufford has been a professional developer on Wall Street for 20 years, specializing in Enterprise scalable systems for both equities and fixed income securities. He is also on the ANSI C++ Technical Committee and the author of Pure C++ (SAMS, 1999). Contact Eric at [email protected].


One of the greatest problems most programmers—especially myself—have is dealing with invalid objects. One of the most cumbersome tasks during the debugging cycle is dealing with issues like null pointer dereference, invalid pointer dereference, and using objects that have been destroyed or otherwise rendered unusable. The Object Registration pattern and the implementation detailed in this article is meant to help identify this code through well formed runtime exceptions that note when and where the violation took place, thus helping to substantially bulletproof your classes.

For example, consider the following code.

HPISocket* pThisSocket;
pThisSocket->User("Eric");

The first line of code declares a pointer to an object, but fails to initialize it. The second line tries to actually use the object, triggering an access violation (core dump in Unix). The code itself is quite innocuous looking, so finding it can be a nightmare. Especially if there is a significant amount of code between the declaration of the pointer and its dereference. The same type of problem occurs when a previously valid object is deleted and later dereferenced, or when these invalid pointers are passed as references to other functions.

The ORV Pattern

Before getting into the meat of how this pattern will help solve these problems, there is one prerequisite for its use. For this pattern to work, one must declare all data private and use member access functions (accessors and mutators) as the sole means by which member variables are accessed. This is, of course, on top of any business-specific processing being performed by other member functions of the class. Its also not a bad idea to have all member functions of the class use the same accessors and mutators, thus ensuring consistent access to the data members of your class. The former is absolutely essential not only for good coding practice, but also for this pattern. The latter is strongly encouraged, but not required. That said, let's get on with it.

class EXPORT HPISocket_c : public ClientSocket_c
{
   private: // CLASS INTEGRITY CHECKING DATA AND FUNCTIONS
    static  const long  m_sclSerialNumber = 0x001001001;
      mutable long  m_mlSerialNumber;
        void    SetSanity()     const throw();
        void    ClearSanity()   const throw();
        void    SanityCheck(int,string const&)  const throw(EXSanity_c);
   private: // DATA MEMBERS
    char        m_cDataClass; // DON'T ASK, DON'T TELL.
    string      m_DBUser;   // USER HITTING THE SERVER
    string      m_DBPassword; // PASSWORD OF SAME
    string      m_HostName;   // SERVER TO HIT
    string      m_UserName;   // 'REAL' USER 
    Items_t     m_Items;      // LIST OF ITEMS TO GET
    Filters_t     m_Filters;      // RESTRICTION CRITERION
    Dictionary_c    m_Dictionary;   // MAP OF ALL VALID FIELD IDs
    ResultSet_t   m_ResultSet;    // ACTUAL RETURNED DATA
    DWORD     m_tTimeLastUsed;  // USED BY LRU ALGORITHM
  public: // CLASS MANAGEMENT FUNCTIONS
        HPISocket_c(string const& User = "",string const& Password = "")  throw(EXSanity_c);
    virtual ~HPISocket_c()      throw(EXSanity_c);
        HPISocket_c(HPISocket_c const& Source)            throw(EXSanity_c);
  public: // OPERATOR OVERLOADS
    bool          operator <  (HPISocket_c const& Source)     throw(EXSanity_c);
    bool          operator == (HPISocket_c const& Source)     throw(EXSanity_c);
    HPISocket_c const&  operator  = (HPISocket_c const& Source)    throw(EXSanity_c);
  public: // EXPOSED FUNCTIONS
    int         GetData()     throw(EXSanity_c);
    bool          EstablishConnection() throw(EXSanity_c);

  private: // INTERNAL FUNCTIONS
    int    SendRequest(stringstream const&)      throw(EXSanity_c);
    int    ReceiveReply(stringstream&)           throw(EXSanity_c);
    int    FormatRequest(stringstream&)          throw(EXSanity_c);
    int    Parse()                               throw(EXSanity_c);
    int    CreateResultSet(stringstream&,Row_t&,Row_t&)     throw(EXSanity_c,EXHPI_c);
    void   Singleton(Row_t&,Value_c const&)               throw(EXSanity_c);
    bool   AppendField(stringstream& Output, Value_c const& Input,
                       Filter_c const& FidFilter) throw(EXSanity_c);
  public: // ACCESSOR MEMBER FUNCTIONS
    string const&     DBUser()      const   throw(EXSanity_c);
    string const&     DBPassword()    const   throw(EXSanity_c);
    string const&     HostName()    const   throw(EXSanity_c);
    string const&     UserName()    const   throw(EXSanity_c);
    Dictionary_c const& Dictionary()    const   throw(EXSanity_c);
    Items_t const&    Items()     const   throw(EXSanity_c);
    Filters_t const&    FilterList()    const   throw(EXSanity_c);
    ResultSet_t const&  ResultSet()   const   throw(EXSanity_c);
    char  const     DataClass()   const   throw(EXSanity_c); 
    unsigned long const Rows()      const   throw(EXSanity_c);
    DWORD const   TimeLastUsed()  const   throw(EXSanity_c);
  public: // MUTATOR MEMBER FUNCTIONS
    string const&     DBUser(string const& x)     throw(EXSanity_c);
    string const&     DBPassword(string const&)     throw(EXSanity_c);
    string const&     HostName(string const&)     throw(EXSanity_c);
    string const&     UserName(string const&)     throw(EXSanity_c);
    Dictionary_c const& Dictionary(Dictionary_c const&)   throw(EXSanity_c);
    Items_t const&    Items(Items_t const&)     throw(EXSanity_c);
    Filters_t const&    FilterList(Filters_t const&)    throw(EXSanity_c);
    char const      DataClass(char)       throw(EXSanity_c);
    DWORD const     TimeLastUsed(DWORD)   throw(EXSanity_c);
    ResultSet_t const&  ResultSet(ResultSet_t const&) throw(EXSanity_c);
  };


Listing 1: Class declaration containing bulletproofing.

The Object Registration and Validation pattern helps solve this problem, and serves as the basis upon which the other patterns—detailed in my book, but not in this article—are built. It's designed to prevent an invalid object from being used in any capacity. Listing 1 shows the entire class declaration, while listing 2 shows the member functions and variables used to implement the pattern. Note: I've included a real-life class declaration to show how accessors and mutators look, and to show that the footprint of the ORV pattern really isn't all that big. And personally, I find code containing the proverbial foo and bar classes as less than trivial, thus not particularly useful.

class EXPORT HPISocket_c : public ClientSocket_c
  {
  // CODE OMMITED FOR BREVITY
   private: // CLASS INTEGRITY CHECKING DATA AND FUNCTIONS
    static  const long  sm_clSerialNumber;
      mutable long  m_mlSerialNumber;
      void    SetSanity()     const throw();
      void    ClearSanity()   const throw();
      void    SanityCheck(int,string const&)  const throw(EXSanity_c);
  // CODE OMMITED FOR BREVITY
  };
Listing 2: Class containing Object Registration code.

Basically, this pattern is composed of two member variables and three member functions that use them. The member functions are SetSanity, ClearSanity and SanityCheck, as shown in listing 3. They are used to initialize the object, clear the object, and check on the state of the object, respectively. Each function is in inline and privately scoped to A) minimize the related overhead and B) make them invisible to consuming objects as well as children of the class in which they are declared.

inline void HPISocket_c::SetSanity() const throw()
{
 m_mlSerialNumber = sm_clSerialNumber;
}

inline void HPISocket_c::ClearSanity() const throw()
{
 SanityCheck(__LINE__,__FILE__);
 m_mlSerialNumber = 0L;
}

inline void HPISocket_c::SanityCheck(int const iLine,string const& File) const throw(EXSanity_c)
{ 
 if((this == NULL) || (m_mlSerialNumber != sm_clSerialNumber))
  throw EXSanity_c(iLine,File,SANITY_FAILURE,EXBase_c::EXError);
}

Listing 3: Definition of the Object Registration functions.

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
}
Listing 4: Using the ORV functions in constructors, destructors and member access.

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;
}
Listing 5: Member Access Functions that use Sanity Checking.

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.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.