GCO4020/CSC428 - Advanced Object Oriented Techniques In C++
Week 3
Sometimes it is desirable to completely separate the implementation¤ of a class's functionality from its interface¤. However, C++ does not facilitate this directly, particularly since the language requires that the protected and private (implementation¤) members of a class be declared at the same time as the public (interface¤) members. This topic describes a simple solution to this problem.
There are two obvious reasons to hide the implementation¤ of a class: to protect commercial software, and/or to ensure that classes are genuine "black boxes". Another, less obvious reason for detaching the implementation¤ of a class is discussed in "Topic 9: Envelopes and Letters".
For obvious reasons, commercial class libraries are rarely released as source code. However, the need to provide header files means that at least some of the source must be exposed to the user. The techniques presented in this topic can reduce this exposure.
More importantly however, the exposure of class internals in each header file (whether commercially sensitive or not) increases the interdependence of the various translation units of a system. In other words, since the class declarations have to include the internal (implementation¤) data members, it is not possible to change the class's implementation¤, without changing the header files, and thereby necessitating widespread recompilation after such a change. Since one of the goals of object-oriented programming is to isolate implementation¤, this dependency may be viewed as a major flaw in the C++ language.
Fortunately, it is not particularly difficult to build classes in which the class interface¤ (as specified by the class declaration in a header file) is entirely independent of the class implementation¤ (as specified by the class member definitions in a source file).
The simplest means of achieving this is to define the interface¤ and implementation¤ of a class in two separate classes:
The resultant class structure looks like this:
The advantage of this separation is that the entire implementation¤ class¤ (and thus the implementation¤ of the interface¤ class¤) need never be seen by the user of the interface¤ class¤. Note that this is only possible because C++ does not require a class to be defined (only declared) before a pointer to that class is created. Hence an interface¤ object may contain a pointer to the implementation¤ class¤ without requiring any knowledge of that class except its name.
By way of example, let us consider a simple class which represents a mapping between an integer key and a string value (see source code listing: HiddenPtr.C). Such a class might appear inside a instantiation of a map-like container template. The interface¤ code in the header file is accessible to users:
// Mapping.h HEADER FILE // =====================
#include <string>
class Mapping { public: Mapping(string name, int value); ~Mapping(void);
void SetName(string name); string GetName(void);
void SetValue(int value); int GetValue(void);
private: class MappingImpl* myImpl; };
and might be used like this:
#include <iostream>
int main(void) { Mapping m1("two",1); Mapping m2("three",1);
m1.SetName("one"); m2.SetValue(3);
cout << m1.GetName() << ": " << m1.GetValue() << endl; cout << m2.GetName() << ": " << m2.GetValue() << endl; }
Note that, so far, we have no idea how the name and value of a Mapping are
stored, nor how the Get... and Set... member functions access them. All that
detail is relegated to a separate source file (which might be pre-compiled and
distributed as an object- or library- file). That separate file would contain
the definitions of Mapping's member functions, as well as the complete
definition of the MappingImpl class:
// Mapping.C SOURCE FILE // =====================
// 1. IMPLEMENTATION CLASS FOR Mapping
class MappingImpl { private: friend class Mapping;
MappingImpl(string name, int value) : myName(name) , myValue(value) {}
void SetName(string name) { myName = name; } string GetName(void) { return myName; }
void SetValue(int value) { myValue = value; } int GetValue(void) { return myValue; }
string myName; int myValue; };
// 2. DEFINITION OF Mapping MEMBER FUNCTIONS
Mapping::Mapping(string name, int value) : myImpl ( new MappingImpl(name,value) ) {}
Mapping::~Mapping(void) { delete myImpl; }
void Mapping::SetName(string name) { myImpl->SetName(name); } string Mapping::GetName(void) { return myImpl->GetName(); }
void Mapping::SetValue(int value) { myImpl->SetValue(value); } int Mapping::GetValue(void) { return myImpl->GetValue(); }
Note first that the MappingImpl class is declared entirely
private, and is therefore accessible only to its friend class,
Mapping. The structure of the class itself is unremarkable, indeed
its data and function members mimic exactly the implementation¤ we
might expect for class Mapping itself, if we were not hiding that
implementation¤.
The member functions of class Mapping are also quite straightforward. The
constructor simply creates a new MappingImpl object and initializes the
Mapping::myImpl pointer member with a pointer to that newly
allocated object. In complement, the ~Mapping destructor cleans up the
implementation¤ object.
The Get... and Set... member functions simply forward the
request to the corresponding member function of the implementation¤
object, returning the corresponding results (if any).
This overall approach is sometimes known as "delegation", because the interface¤ class¤ delegates all its actual duties to the implementation¤ class¤.
See also: Exercise 1
See also: Exercise 2
See also: Exercise 3
One major drawback of this above technique is that it interposes a pointer between the interface¤ and the implementation¤ classes¤, and hence imposes the cost of a dereference on every access. In fact, there is no reason why we should not "pre-dereference" the pointer to (potentially) eliminate these costs.
We can achieve this by replacing the MappingImpl* in the Mapping class
by a MappingImpl&, as follows (also see source code listing: HiddenRef.C):
// Mapping.h HEADER FILE (REFERENCE VERSION) // =========================================
#include <string>
class Mapping { public: Mapping(string name, int value); ~Mapping(void);
void SetName(string name); string GetName(void);
void SetValue(int value); int GetValue(void);
private: class MappingImpl& myImpl; // <-- THE ONLY CHANGE IS HERE };
The corresponding changes to the source file are likewise minor:
// Mapping.C SOURCE FILE (REFERENCE VERSION) // =========================================
// 1. IMPLEMENTATION CLASS FOR Mapping // (OMITTED HERE - NO CHANGE)
// 2. DEFINITION OF Mapping MEMBER FUNCTIONS
Mapping::Mapping(string name, int value) : myImpl ( *new MappingImpl(name,value) ) {}
Mapping::~Mapping(void) { delete &myImpl; }
void Mapping::SetName(string name) { myImpl.SetName(name); } string Mapping::GetName(void) { return myImpl.GetName(); }
void Mapping::SetValue(int value) { myImpl.SetValue(value); } int Mapping::GetValue(void) { return myImpl.GetValue(); }
There is no change at all to the MappingImpl class, and only
three minor changes to Mapping:
Mapping constructor, the pointer
returned by new is dereferenced during the
initialization of myImpl (this is the
"pre-dereferencing" suggested above),
Mapping destructor, the reference held in
myImpl must be "re-referenced" (that is, have its
address computed using the & operator) in order to
be able to pass a pointer to delete,
Mapping::Set... and
Mapping::Get... member functions, the
appropriate member functions of the MappingImpl
object are accessed via the dot operator (since
myImpl is a reference), not the arrow operator
(which is only used on pointers).
The actual benefit is that the use of a reference guarantees that the
identity of the object accessed via myImpl is "fixed" for the
lifetime of each Mapping object, and hence cannot accidentally be
"lost" by an erroneous assignment to myImpl. Such "losses" may
seem unlikely, but as the complexity of the implementation¤ class¤ grows
(probably through the efforts of numerous successive maintainers),
errors like this are increasingly likely to creep in, particularly if other
pointer-based implementation¤ techniques
(for example, Topic 7: Reference Counting) are also employed.
The second potential benefit of this approach is that a sufficiently
clever compiler can make use of the guaranteed "fixedness" of the myImpl
reference in order to eliminate the per-access dereference (and
possibly achieve other optimizations as well).
For example, a simple test of the two versions of implementation¤
hiding (pointer-based and reference-based) compiled under GNU
g++ with full optimization indicates that
Mapping::Set... member functions execute approximately 5% faster
when references are used instead of pointers.
See also: Exercise 4
Last updated: Fri Feb 18 11:17:16 2000