GCO4020/CSC428 - Advanced Object Oriented Techniques In C++
Week 3

 

Topic 5: Hiding Implementation Details

Introduction

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.


Synopsis


Source Code Examples


Why hide the implementation¤?

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.


Hiding the implementation¤ behind a pointer

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 two classes are linked by a single private pointer member in the interface¤ class¤. Note that, for this reason, interface¤ classes¤ are occasionally referred to as "pimple" classes, because they contain a pointer to the implementation class.

The resultant class structure looks like this:


                [Diagram showing two classes: "interface" (the
                 declaration of which is accessible, the definition of
                 which is not), and "implementation" (which is totally
                 inaccessible)]

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


Hiding the implementation¤ behind a reference

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:

This approach to hiding implementation¤ details confers two extra benefits - one actual and one potential - as compared to the pointer-based solution.

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


Exercises

There are 4 exercises associated with this topic.

 


This material is part of the GCO4020/CSC428 - Advanced Object Oriented Techniques In C++ course.
Copyright © Damian Conway, 1997. All rights reserved.

Last updated: Fri Feb 18 11:17:16 2000