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
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.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru