Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 21.  Veneers


21.2. Binding Data and Operations

When using data structures and a (C-)API (see section 3.2), it is common practice to create wrapper classes for the data structures and to take advantage of C++'s built-in support for resource cleanup through the mechanism of object destruction. As we know all too well from experience, it is also common to quickly accrete functionality for doing "just that one extra feature" and end up with a bloated class with lots of dependencies.

As we saw in Chapters 3 and 4, life in C++ is thankfully a lot richer than some instructional texts would have us believe, where types exist either as plain-old-data (POD; see Prologue), or as rich classes with tight access control and fully fledged semantics. Often what is needed is something in the middle of the spectrum. For now, let's assume we only need to be part way along.

The COM VARIANT type is a discriminated union, in the form of a 16-byte structure that is used to contain, or hold pointers to, C and COM types. Because COM is not C++-specific, VARIANT is a C-compatible type. Hence the VARIANT API is a set of C functions that initialize, copy, and uninitialize the structures. Obviously such an arrangement—raw structures (sometimes) holding allocated resources—is a leading candidate for wrapping in the safety of a C++ class, and there have been several developed, such as CComVariant (Active Template Library), COleVariant (Microsoft Foundation Classes), _variant_t (provided with the Visual C++ compiler), and my own Variant. While there are legitimate criticisms of all these classes, I'm not necessarily saying that any particular ones are bad or shouldn't be used. What we're going to do is see if they are necessary, and in so doing exemplify another kind of veneer.

The work I'm going to describe is an extensible type-safe generic logging subsystem, based on previous work of a lesser scope that I designed for a medium-traffic application server a couple of years ago. The requirements were:

  • Message-based— facilitate multiple language support

  • Instance-oriented— messages are associated with application instances

  • Pluggable— the ability to plug in different log recipients (file, network, memory) at run time

  • Efficient— minimum amount of data traveling; minimum call cost in the calling code

  • General— can accommodate a wide (though limited) range of types

  • Extensible— new message ids can be added without breaking existing code

  • Type safe— no printf(…)-like opacity; must break at compile time if a message definition is changed such that the argument list is changed

To cater for these requirements, the foundation of the API is the ILog interface, which looks like the following:



struct ILog


{


  virtual void Log( AIID_t const  &instanceId,


                    MsgId_t const &msgId,


                    size_t        cArgs,


                    VARIANT       args[]) = 0;


};



Different implementing instances could be plugged in—via a SetLog() function—even chained together, at the will of the implementer of the given client process. The current active log was accessed via the free-function GetLog(). Hence one could issue a log entry by the following:



GetLog()->Log(instId, msgId, 2, &args[0]);



That seems to give us the message-based, instance-oriented, pluggable, and general characteristics, but we're still some way from a foolproof (and attractive[1]) system.

[1] Software is a lot like maths. Oftentimes if your solution doesn't feel elegant, it's likely not ready.

21.2.1 pod_veneer

If we want to have a type safe yet general approach to the types of the arguments, it suggests that templates are going to be involved. The first template we need is the pod_veneer template, which applies immutable RAII (see section 3.5) to POD types.

Listing 21.2.



template< typename T


        , typename CF // Constructor function


        , typename DF // Destructor function


        >


class pod_veneer


  : public T


{





  typedef pod_veneer<T, CF, DF>   class_type;


public:


  pod_veneer()


  {


    CF()(static_cast<T*>(this));  // Construct the pod


  }


  ~pod_veneer()


  {


    constraint_must_be_pod(T);


    constraint_must_be_same_size(class_type, T);


    DF()(static_cast<T*>(this));  // Destroy the pod


  }


};



The veneer takes a POD-type parameter, from which it inherits, along with two functor types. In the constructor it executes the constructor function type on itself, and in the destructor it executes the destructor function type on itself; in effect it binds operations to the raw (POD) data. Note also that it uses two constraints. The constraint_must_be_pod() is used to ensure that its primary parameterizing type is POD. The constraint_must_be_same_size() (see section 1.2.5) is used to ensure that the parameterization of the template has obeyed EDO. So how does this help us with our logging API?

Clearly we're going to use the pod_veneer on the VARIANT type. We could define the following construction and destruction functors:

Listing 21.3.


struct variant_init


{


  void operator ()(VARIANT &var)


  {


    ::VariantInit(&var);


  }


};


struct variant_clear


{


  void operator ()(VARIANT &var)


  {


    ::VariantClear(&var);


  }


};



These functors simply translate the requisite functions from the VARIANT API into a form digestible to the template; because pod_veneer is a class template, we cannot use the VariantInit() and VariantClear() functions to instantiate it as we would if it was a function template.

21.2.2 Creating Log Messages

The generated class MSG_BASE is used to provide the translation from a generic set of parameters to a call to Log::Log(). To do this it takes the arguments, packs them into an appropriately sized array of pod_veneer<VARIANT, . . .>, and writes that array to the log.

Listing 21.4.



typedef pod_veneer< VARIANT


                  , variant_init


                  , variant_clear


                  >     VARIANT_veneer;





class MSG_BASE


{


public:


  template <typename T1>


  MSG_BASE(AIID_t const &aiid, MsgId_t const &msgId, T1 const &a1)


  {


    VARIANT_veneer  args[1];


    InitialiseVariant(args[0], a1);


    STATIC_ASSERT(dimensionof(args) == 1);


    GetLog()->Log(aiid, msgId, dimensionof(args), args);


  }


  template <typename T1, typename T2>


  MSG_BASE(AIID_t const &aiid . . ., T1 const &a1, T2 const &a2)


  {


    VARIANT_veneer  args[2];


    InitialiseVariant(args[0], a1);


    InitialiseVariant(args[1], a2);


    STATIC_ASSERT(dimensionof(args) == 2);


    GetLog()->Log(aiid, msgId, dimensionof(args), args);


  }


  template <typename T1, typename T2, typename T3>


  MSG_BASE(AIID_t const &aiid . . . T1 const &a2, T1 const &a3)


  {


    VARIANT_veneer  args[3];


    InitialiseVariant(args[0], a1);


    InitialiseVariant(args[1], a2);


    InitialiseVariant(args[2], a3);


    STATIC_ASSERT(dimensionof(args) == 3);


    GetLog()->Log(aiid, msgId, dimensionof(args), args);


  }


  . . . // Constructors with more parameters


// Not implemented


private:


  . . . // Hide copy ctor/assignment op


};



The InitialiseVariant functions are a suite of overloaded inline free functions that initialize a VARIANT from a set of types and that do not throw any exceptions.



void InitialiseVariant(VARIANT &var, char const *v);


void InitialiseVariant(VARIANT &var, wchar_t const *v);


void InitialiseVariant(VARIANT &var, uint16_t v);


void InitialiseVariant(VARIANT &var, sint32_t v);


void InitialiseVariant(VARIANT &var, double v);


. . .



21.2.3 Reducing Waste

In fact, there's actually inefficiency in the picture I just painted. Because the overload conversion functions initialize the VARIANT before instantiating it, we don't need to initialize the variant in the array construction; we can do nothing, knowing that the initialization functions will handle this for us. The actual implementation uses noop_function<VARIANT>, which does surprisingly little.

Listing 21.5.


template <typename T>


struct noop_function


    : public std::unary_function<T const &, void>


{


  void operator ()(T const & /* t */)


  {} // A minimalist functor


};





typedef pod_veneer< VARIANT


                  , noop_function<VARIANT>


                  , variant_clear


                  >     VARIANT_veneer;



Now we've actually taken a step back from RAII to RRID (see section 3.4). This is appropriate in this instance because the log API does not throw exceptions, nor do the initialization functions: if they cannot allocate some resources to initialize the VARIANT with the appropriate value, they are guaranteed to initialize it to VT_EMPTY. This is an artifact of the execution context dictated by the requirements of the logging subsystem. In the general case you may choose to throw allocation failure exceptions in the initialization functions (though that does leave the question as to who's going to log the failure of the log), in which case you would use variant_init as originally described.

There are two important features to note about what we've done. First, we've ensured that the initialized VARIANT array instances are destroyed, no matter what, because the destructor of the pod_veneer will call the function operator of variant_clear for each constructed instance. The language guarantees that this will happen. Of course, this would be the case with a full-fledged VARIANT class. However, we know that an array of pod_veneer<VARIANT, ...> is safely convertible to VARIANT* because pod_veneer guarantees this to be the case.

Another variant class may exhibit the same behavior, but it may not. You won't find out at compile time (see section 1.4), and there's nothing to stop an update to the class implementation from changing it even if it currently works.

The second feature is an issue of efficiency. Because in the actual implementation we guaranteed that the InitialiseVariant() functions do not throw, and null initialize the variants on failure, we chose to have pod_veneer do no work in its constructors. This was achieved by parameterizing it with another type, a type that could be a member type of MSG_BASE. Hence we can stipulate behavior by policy, and that policy type can be centralized. This provides a great deal of flexibility when the API is put to use on another project or the assumptions of the current project change. The only ways to change policy in the same way when using VARIANT wrapper classes from external sources are either to place them in parameterizable veneers or to select different wrapper classes by policy. Neither of those options is attractive from either a maintenance or effort point of view. Remember that there is no reason other than automatic destruction for having fully fledged VARIANT wrapper classes in this context. We must use the InitialiseVariant() functions, in order for the generic log-argument mechanism to work. Hence, the only functionality we need wrapped up in objects is resource initialization and resource destruction. We've got all the power we need, and none of the dependencies or costs (i.e., inefficiencies) we don't need.

21.2.4 Type-safe Message Classes

We've seen the point of the pod_veneer, but I'd like to just finish off the discussion by describing how it all fits together, with all the requirements fulfilled. The message ids are maintained in a database, from which two files are produced. One is a binary file for use with a fast message lookup mechanism when the logs are being inspected (either at run time or offline). The other is a header with definitions of generated message classes such as the following:

Listing 21.6.


class MSG_RESOURCE_NOT_FOUND


  : public MSG_BASE


{


public:


  MSG_RESOURCE_NOT_FOUND(UINT id)


    : MSG_BASE(AIID_NULL, MSGID_RESOURCE_NOT_FOUND, id)


  {}


  MSG_RESOURCE_NOT_FOUND(AIID_t const &instanceId, UINT id)


    : MSG_BASE(instanceId, MSGID_RESOURCE_NOT_FOUND, id)


  {}


};


class MSG_BAD_CAST


  : public MSG_BASE


{


public:


  MSG_BAD_CAST(char const *expression, int level)


    : MSG_BASE(AIID_NULL, MSGID_BAD_CAST, expression, level)


  {}


  MSG_BAD_CAST(AIID_t const &instanceId, char const *expr, int level)


    : MSG_BASE(instanceId, MSGID_BAD_CAST, expr, level)


  {}


};



Messages are logged by using temporaries of these classes, as in:



if(!resmgr.Contains(rsrcId))


{


  MSG_RESOURCE_NOT_FOUND(rsrcId);


}



So how does it square up? Have we achieved the things—over and above those provided by the Log interface API—that were stipulated in our requirements?

  • Type safety: It is as type safe as it is possible to get with C++, that is, 100 percent but for a few implicit integer conversions, and possible confusion over 0 (although we could use safer NULL now, which would help).

  • Extensibility: New messages are defined in the database, and their type-safe wrapper classes, with exact arguments, are generated automatically.

  • Generality: It works with any VARIANT-compatible type.

  • Efficiency: Everything that can be inlined is inlined, and we are able to parameterize our veneer to prevent unnecessary work being done.


      Previous section   Next section