MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Handle classes

Access control in C++ allows you to separate interface from implementation, but the implementation hiding is only partial. The compiler must still see the declarations for all parts of an object in order to create and manipulate it properly. You could imagine a programming language that requires only the public interface of an object and allows the private implementation to be hidden, but C++ performs type checking statically (at compile time) as much as possible. This means that you’ll learn as early as possible if there’s an error. It also means your program is more efficient. However, including the private implementation has two effects: The implementation is visible even if you can’t easily access it, and it can cause needless recompilation.

Visible implementation

Some projects cannot afford to have their implementation visible to the end user. It may show strategic information in a library header file that the company doesn’t want available to competitors. You may be working on a system where security is an issue – an encryption algorithm, for example – and you don’t want to expose any clues in a header file that might enable people to crack the code. Or you may be putting your library in a “hostile” environment, where the programmers will directly access the private components anyway, using pointers and casting. In all these situations, it’s valuable to have the actual structure compiled inside an implementation file rather than exposed in a header file.

Reducing recompilation

The project manager in your programming environment will cause a recompilation of a file if that file is touched or if another file it’s dependent upon – that is, an included header file – is touched. This means that any time you make a change to a class, whether it’s to the public interface or the private implementation, you’ll force a recompilation of anything that includes that header file. For a large project in its early stages this can be very unwieldy because the underlying implementation may change often; if the project is very big, the time for compiles can prohibit rapid turnaround.

The technique to solve this is sometimes called handle classes or the “Cheshire Cat”[23] – everything about the implementation disappears except for a single pointer, the “smile.” The pointer refers to a structure whose definition is in the implementation file along with all the member function definitions. Thus, as long as the interface is unchanged, the header file is untouched. The implementation can change at will, and only the implementation file needs to be recompiled and relinked with the project.

Here’s a simple example demonstrating the technique. The header file contains only the public interface and a single pointer of an incompletely specified class:

//: C05:Handle.h
// Handle classes
#ifndef HANDLE_H
#define HANDLE_H

class Handle {
  struct Cheshire; // Class declaration only
  Cheshire* smile;
public:
  void initialize();
  void cleanup();
  int read();
  void change(int);
};
#endif // HANDLE_H ///:~ 

This is all the client programmer is able to see. The line

struct Cheshire;

is an incomplete type specification or a class declaration (A class definition includes the body of the class.) It tells the compiler that Cheshire is a structure name, but nothing about the struct. This is only enough information to create a pointer to the struct; you can’t create an object until the structure body has been provided. In this technique, that body contains the underlying implementation and is hidden away in the implementation file:

//: C05:Handle.cpp {O}
// Handle implementation
#include <cstdlib>
#include "../require.h"
#include "Handle.h"
using namespace std;

// Define Handle's implementation:
struct Handle::Cheshire {
  int i;
};

void Handle::initialize() {
  smile = (Cheshire*)malloc(sizeof(Cheshire));
  require(smile != 0);
  smile->i = 0;
}

void Handle::cleanup() {
  free(smile);
}

int Handle::read() {
  return smile->i;
}

void Handle::change(int x) {
  smile->i = x;
} ///:~ 

Cheshire is a nested structure, so it must be defined with scope resolution:

struct Handle::Cheshire {

In the Handle::initialize( ), storage is allocated for a Cheshire structure, [24] and in Handle::cleanup( ) this storage is released. This storage is used in lieu of all the data elements you’d normally put into the private section of the class. When you compile Handle.cpp, this structure definition is hidden away in the object file where no one can see it. If you change the elements of Cheshire, the only file that must be recompiled is Handle.cpp because the header file is untouched.

The use of Handle is like the use of any class: Include the header, create objects, and send messages.

//: C05:UseHandle.cpp
//{L} Handle
// Use the Handle class
#include "Handle.h"

int main() {
  Handle u;
  u.initialize();
  u.read();
  u.change(1);
  u.cleanup();
} ///:~ 

The only thing the client programmer can access is the public interface, so as long as the implementation is the only thing that changes, this file never needs recompilation. Thus, although this isn’t perfect implementation hiding, it’s a big improvement.


[23] This name is attributed to John Carolan, one of the early pioneers in C++, and of course, Lewis Carroll.

[24] Chapter 11 demonstrates a much better way to create an object on the heap with new.

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru