Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 4.  Data Encapsulation and Value Types


4.9. Encapsulation Coda

We've now looked at the value type concept from a theoretical perspective, but the question remains as to how such types are to be encapsulated. In instructional texts, we are directed to be good object-oriented citizens and fully encapsulate our types. However, in the real world, things are rarely so clear.

At this time I'll suggest seven possible implementation of the UInteger64 type, only three of which use full encapsulation. In order to select the appropriate form, there are several salient questions we need to ask ourselves:

Do we need to interoperate with, or be implemented in terms of, an underlying C API? If so, we'll want to contain (form 2), or inherit from (form 3), an existing structure, as otherwise we will not be able to pass the appropriate parts of the internal structure of the class to the C API.[7] If we don't, then we can simply contain the basic members (form 1).

[7] Well, not without a lot of packing pragmas, casting, and other inexcusable hacks. Better to just use the C structure.

Listing 4.2.


// form #1


class UInteger64


{


  . . . // Value type methods and operators





private:


  uint32_t  lowerVal;


  uint32_t  upperVal;


};





// form #2


class UInteger64


{


  . . . // Value type methods and operators





private:


  uinteger64  m_value;


};





// form #3


class UInteger64


  : private uinteger64


{


  . . . // Value type methods and operators


};



Remember that I've deliberately chosen 64-bit integers because I can cheat in the actual implementation by using conversions to and from real 64-bit integers, which are used to realize the actual arithmetic operations. If we wanted to provide integers of arbitrary size layered over C APIs (as in [Hans1997]), we have no choice but to work with C structures.

Can all operations be encapsulated within the type? If so, then we can probably provide full encapsulation by making the internal implementation private (forms 1–3) If not, then we are going to have to leak out some internal state in order to interact with other functions and/or types, although friendship (section 2.2.9) can be used with the latter. There are several related questions that determine whether we will need to expose implementation details to the outside world.

Is the type we are creating the "one true type" or rather just one of many? The classic miscreant in this regard is the handling of time. There are time_t, struct tm, struct timeval, DATE, FILETIME, SYSTEMTIME, to name a few. And those are just C time types. If we include C++ classes, the list is potentially limitless. Who's not implemented their own date/time type? If it is one of many, we must consider the need to interact and intercovert with other types.

Do we need to interact with a C-compatible interface? Of course, the ideal answer to this is always no, but unfortunately the real answer is often yes. If we consider a date/time class, we're going to base it on one of the C-types, because to do otherwise would require that we rewrite all the complex, tedious, and error-prone calendar manipulation code. Leap years, Gregorian time, orbital corrections, anyone? No thanks. Given this, how can we be sure we've encapsulated all the necessary functionality?

It is the case that the C++ community as a rule ignores the issues that inform on this particular question, which I personally believe is a great disservice to the development community.[8] Vendors are complicit in this deception, since the more locked-in a developer is to some "really useful class," the less likely he/she is to seek alternatives. And the ramifications of being locked-in can be greater than just preferring one application framework to another. I'm sure we've all known projects that have been locked-in to a particular operating system because the developers were not comfortable, or able, to escape the confines of their application framework. This is a big fat imperfection in C++, which we'll deal with in no small way in Part Two.

[8] Indeed, several reviewers for this book frequently complained that I should not be including any examples of C-compatible code in a book ostensibly about C++.

It's clear that in many practical circumstances we are forced to use only partial encapsulation. There are several ways of doing this. The simple way is to change the access specifier from private to public (forms 4–6), but this is, in effect, leaking out everything.

Listing 4.3.


// form #4


class UInteger64


{


  . . . // Value type methods and operators


public:


  uint32_t  lowerVal;


  uint32_t  upperVal;


};





// form #5


class UInteger64


{


  . . . // Value type methods and operators


public:


  uinteger64  m_value;


};





// form #6


class UInteger64


  : public uinteger64


{


  . . . // Value type methods and operators


};



There are ways of being a bit more circumspect about how much is given out. An effective, but thoroughly inaesthetic, way is to provide accessor functions.



// form #6


class UInteger64


  : private uinteger64


{


  . . . // Value type methods and operators


public:


  uinteger64 &get_uinteger64();


  uinteger64 const &get_uinteger64()const; // Ugly!


};



There's another way to make this ugly stuff a bit nicer: explicit_cast, which we examine in more detail in sections 16.4 and 19.5.

Listing 4.4.


// form #7


class UInteger64


  : private uinteger64


{


  . . . // Value type methods and operators


public:


  operator explicit_cast<uinteger64 &>();


  operator explicit_cast<uinteger64 const &>() const;


};



Implicit access to the internals is denied, but explicit access is achievable. The alternative is to add methods to the class for each new feature in the underlying API that you wish to make available to users of the superior class form.

Closely related to this are two final questions, which can further inform on our choices:

Will efficiency concerns require us to break orthogonality? Some desirable operations on our types could be the logical combination of two simpler operations, but combining them into one operation can give significant efficiency benefits. Worse, sometimes this is done simply for convenience. Either way is the first step on the path to fat classes with a woeful number of methods (e.g., std::basic_string<>). The decision here must be on a case-by-case basis, but remember to only put a lot of stall by efficiency for methods that genuinely represent bottlenecks, through extent and/or frequency of use, and based on measurement rather the often fallible (in this regard) instinct.

Are we confident that we can avoid (link-time and compile-time) coupling? Again, this is a much underdiscussed aspect of class design. If the entire implementation of the type can be efficiently expressed in an inline definition then we don't have to worry at all about link-time coupling—the linking to function implementations, either compiled in a separate compilation in the current link-unit, or in external static/dynamic libraries. Either way, we still have to worry about compile-time coupling—the number of other files that must be included in order to compile our class. It's a cruel irony that often the reduction of link-time coupling can increase compile-time coupling.

Unfortunately, avoiding compile-time coupling is hard to achieve, and ironically the more you try for portability, the more compile-time coupling you introduce. This is because you'll have to handle nonstandard types (section 13.2), compiler-features, calling-conventions (Chapter 7), and so on. Such things are sensibly factored out into common, well-tested, header files, which necessarily grow as they mature, in order to centralize architecture/compiler/operating-system/library discrimination.

As you can see, data encapsulation is not a straightforward matter, and, as with so many other issues we examine in this book, the only real solution is to be mindful of all the issues. Your classes need to care about more than just the specific environment in which you create them, and since they're not clever, you have to be.


      Previous section   Next section