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.4. Open Types

4.4.1 POD Open Types

Open types are simple types whose member data are publicly accessible, and usually have no methods at all: aggregate types (C++-98: 8.5.1;1) in other words. Consider the uinteger64 type:



struct uinteger64


{


  uint32_t  lowerVal;


  uint32_t  upperVal;


};



This structure is very simple indeed. However, it's not in the least bit usable, and is, therefore, a perfect example of an open value type since such types are, by and large, pretty unusable.

Attempting to use open types as value types is taxing, to say the least. They cannot take part in simple comparison expressions, and manipulating them arithmetically is only possible using manual manipulation of their constituent parts.



uinteger64  i1 = . . .;


uinteger64  i2 = . . .;


bool        bLess   = i1 < i2;  // Compiler error!


bool        bEqual  = i1 == i2; // Compiler error!


uinteger64  i3      = i1 + i2;  // Compiler error!



We should be grateful that C++ rejects all of these operators by default, since at least that affords us protection at compile time. Doing a memberwise comparison would be very dangerous. It might work in most cases for (in)equality, but how would the compiler know which order to prioritize the member variables in a less than comparison? (Note, however, that for backward compatibility with structs, copy construction and copy assignment [see section 2.2] are accepted by the compiler.)

Despite the serious usability issues, there are times when we have to use types such as this, usually because we're interfacing to an operating system or library API, for example, to get extended precision arithmetic [Hans1997]. For illustrative purposes, we're going to assume that we've got good reasons for proceeding with our 64-bit integer types, and define an API to deal with them.



void    UI64_Assign( uinteger64 *lhs, uint32_t higher


                   , uint32_t lower);


void    UI64_Add( uinteger64 *result, uinteger64 const *lhs


                , uinteger64 const *rhs);


void    UI64_Divide( uinteger64 *result, uinteger64 const *lhs


                   , uinteger64 const *rhs);


int     UI64_Compare(uinteger64 const *lhs, uinteger64 const *rhs);


#define UI64_IsLessThan(pi1, pi2)     (UI64_Compare(pi1, pi2) < 0)


#define UI64_IsEqual(pi1, pi2)        (0 == UI64_Compare(pi1, pi2))


#define UI64_IsGreaterThan(pi1, pi2)  (0 < UI64_Compare(pi1, pi2))



Using the API, the previous code can be implemented legally:



uinteger64  i1 = . . .;


uinteger64  i2 = . . .;


bool        bLess   = UI64_IsLessThan(i1, i2);


bool        bEqual  = UI64_IsEqual(i1, i2);


uinteger64  i3;





UI64_Add(&i3, &i1, &i2);



But it's horrible stuff. Naturally C++ let's us do a lot better than this.

4.4.2 C++ Data Structures

Before we all go rushing to our source databases to convert every struct to a class in an object-oriented blooding frenzy, it's important to emphasize that the open types we've been talking about so far are those where the individual members together represent some logical whole, and whose independent manipulation therefore represents a manifest risk to the logical condition.

There are also those open types that are quite safely manipulated in this fashion and that cannot be represented as being a danger. The important distinction is whether individual manipulation of the constituent fields leads to a significant breaking of meaning.

Consider the following currency type:



struct Currency


{


  int majorUnit; // Dollars, pounds


  int minorUnit; // Cents, pence


};



This is a dangerous open type, since it is possible to add a value to the minorUnit that will make the logical value of a Currency instance invalid. However, the following is an eminently reasonable open type:



struct Patron


{


  String    name;


  Currency  wallet;


};



The name and wallet fields are not intrinsically linked, and a change to the name and/or the wallet fields does not result in a prima facie breaking of the logical coherence of a Patron type. Such types are referred to as (C++) data structures [Stro1997].

Naturally, there's a gray area in these matters, but the two cases above are both clear-cut: one's black and the other's white.


      Previous section   Next section