Insulation
This chapter explains how to reduce unnecessary compile-time coupling between components.
Insulation:
The physical design process of avoiding or removing unnecessary compile-time coupling.
Insulation is the physical analog of encapsulation.
Encapsulation
└── Prevents programmatic access to implementation details
Insulation
└── Prevents implementation changes from forcing client recompilation
6.1 From Encapsulation to Insulation
A component may completely encapsulate its implementation logically while still exposing implementation details physically through its header file.
Insulated Implementation Detail:
An implementation detail that can be altered, added, or removed without forcing clients to recompile.
Header Contains Implementation Detail
↓
Implementation Changes
↓
Clients Recompile
Insulated Implementation
↓
Implementation Changes
↓
Clients Do Not Recompile
Private data is encapsulated from clients, but if it appears in the class definition, it is not insulated.
For example:
class Shape
{
int d_x;
int d_y;
public:
// ...
};
Changing:
int d_x;
int d_y;
to:
short d_x;
short d_y;
does not change the logical interface, but clients still need to recompile because the physical layout changes.
6.1.1 The Cost of Compile-Time Coupling
Compile-time coupling becomes increasingly expensive as systems grow.
When many translation units include large amounts of header information:
- More source must be parsed.
- More components are affected by header changes.
- Small implementation changes can trigger large rebuilds.
- Development and maintenance become slower.
The chapter's experiment shows that compile-time cost can grow disproportionately as the amount of included header information increases.
Key Principle:
Widely Used Low-Level Header
↓
Many Compile-Time Dependents
↓
Small Header Change
↓
Large Recompilation Cost
Insulation is therefore especially important for widely used and frequently changing interfaces.
6.2 C++ Constructs and Compile-Time Coupling
Implementation details become part of the physical interface whenever they must appear in a header file.
The chapter identifies the following major sources of compile-time coupling:
- Inheritance
- Layering
- Inline functions
- Private members
- Protected members
- Compiler-generated functions
- Include directives
- Default arguments
- Enumerations
6.2.1 Inheritance and Compile-Time Coupling
Inheritance Dependency:
A derived class requires the complete definition of its base class.
class Derived : public Base
{
};
The header defining Derived must have access to the complete definition of Base.
This applies even to private inheritance.
Base Header Changes
↓
Derived Recompiles
↓
Clients of Derived Recompile
Inheritance therefore cannot be insulated from clients while it remains part of the class definition.
6.2.2 Layering and Compile-Time Coupling
HasA
HasA:
Embedding an object directly as a data member.
class Stooges
{
Moe d_moe;
};
The compiler must know the complete layout of Moe.
Therefore:
HasA
└── Requires complete type
└── Compile-time coupling
HoldsA
HoldsA:
Holding another object through a pointer or reference.
class Larry;
class Curly;
class Stooges
{
Larry* d_larry_p;
Curly& d_curly;
};
The complete physical layout of Larry or Curly is not necessarily required in the header.
HoldsA
└── Forward declaration may be sufficient
└── Better insulation
6.2.3 Inline Functions and Compile-Time Coupling
An inline function intended for use by clients must have its definition available in the header.
inline int value() const
{
return d_value;
}
Therefore the function body becomes part of the physical interface.
Consequences include:
- Changing the inline implementation forces clients to recompile.
- Changing a function from inline to non-inline, or vice versa, forces recompilation.
- Types used substantively inside the inline body can become compile-time dependencies.
- Objects used or returned by value may require complete type definitions.
Inline Function
↓
Implementation in Header
↓
Implementation Not Insulated
6.2.4 Private Members and Compile-Time Coupling
Private members are logically encapsulated but physically visible in the class definition.
This includes:
- Private data members.
- Private member-function declarations.
class String
{
char* d_string_p;
int d_length;
void copy(const String&);
public:
// ...
};
Changing or removing d_length forces clients to recompile.
Changing the signature of copy also modifies the header and forces recompilation.
Key Distinction:
private
└── Encapsulated
private in header
└── Not necessarily insulated
6.2.5 Protected Members and Compile-Time Coupling
Protected members are intended for derived classes but remain visible in the physical interface.
Changing the protected interface can force recompilation of:
- Clients of the base class.
- Derived classes.
- Clients of the derived classes.
Principle:
Supporting derived-class authors through protected base-class members exposes ordinary clients to uninsulated implementation details.
Protected data is particularly undesirable when insulation is an important design goal.
6.2.6 Compiler-Generated Member Functions and Compile-Time Coupling
The compiler may generate functions such as:
- Copy constructor.
- Assignment operator.
- Destructor.
If a later implementation change requires explicitly declaring one of these functions, the class definition changes.
Implicit Function
↓
Implementation Change
↓
Explicit Declaration Required
↓
Header Changes
↓
Clients Recompile
Compiler-generated behavior can therefore create an unexpected compile-time dependency on implementation decisions.
6.2.7 Include Directives and Compile-Time Coupling
Every #include in a header exposes clients to the included header's physical interface.
Bad pattern:
// bank.h
#include "bankcard.h"
#include "germanmarks.h"
#include "japaneseyen.h"
#include "usdollars.h"
// ...
A client that needs only one part of Bank becomes compile-time dependent on all included types.
Unnecessary Include Dependency:
Compile-time coupling introduced by including a header whose complete definition is not actually required.
Header A
├── includes B
├── includes C
└── includes D
Client includes A
↓
Client also depends on B, C, D
Do not include definitions merely for client convenience when forward declarations are sufficient.
6.2.8 Default Arguments and Compile-Time Coupling
Default argument values are part of the function declaration in the header.
class Circle
{
public:
Circle(double x = 0,
double y = 0,
double radius = 1);
};
Changing a default value requires clients to recompile.
Default Argument
└── Compiled into client code
6.2.9 Enumerations and Compile-Time Coupling
Definitions such as enumerations and typedefs must be visible to clients that use them.
A large common-definitions header creates widespread coupling.
Bad organization:
sysdefs.h
├── Unrelated typedefs
├── Unrelated constants
├── Large status enum
└── Global definitions
Almost every component
└── includes sysdefs.h
Adding a single definition can force much of the system to recompile.
Principle:
Granting higher-level clients authority to modify the interface of a lower-level shared resource implicitly couples all clients.
Prefer placing definitions with the abstraction that owns and uses them.
6.3 Partial Insulation Techniques
Insulation does not have to be all-or-nothing.
Partial Insulation:
Removing selected implementation details from the physical interface while leaving others exposed.
More Insulated Details
↓
Fewer Client Recompilations
The amount of insulation should reflect how likely a detail is to change and how many clients would be affected.
6.3.1 Removing Private Inheritance
Private inheritance is an implementation detail but creates unavoidable compile-time coupling.
A common transformation is:
WasA
↓
HoldsA
Instead of:
class Derived : private Base
{
};
prefer, where appropriate:
class Base;
class Derived
{
Base* d_base_p;
};
This can remove the base-class definition from the physical interface.
6.3.2 Removing Embedded Data Members
Embedded objects create HasA compile-time dependencies.
A common transformation is:
HasA
↓
HoldsA
Instead of:
class X
{
Y d_y;
};
use an opaque pointer when appropriate:
class Y;
class X
{
Y* d_y_p;
};
The complete definition of Y can then move to the implementation file.
6.3.3 Removing Private Member Functions
Private member functions are declared in the class definition and therefore are not insulated.
When a helper function does not require member-function status, move it to file scope in the implementation file.
Instead of:
class X
{
void helper();
};
use:
// x.c
static void helper()
{
// ...
}
The helper can now change without altering the component header.
6.3.4 Removing Protected Members
Protected functions expose implementation support for derived classes through the public physical interface.
Possible approaches include:
- Move common functionality into a separate utility component.
- Extract a protocol class.
- Move implementation support out of the protected interface.
The goal is to avoid forcing ordinary clients to see details intended only for derived-class authors.
6.3.5 Removing Private Member Data
Private data can sometimes be removed from the physical interface by:
- Extracting a protocol.
- Moving static implementation data to file scope in the implementation file.
- Replacing concrete representation with opaque storage.
The important goal is to keep representation changes from modifying the public header.
6.3.6 Removing Compiler-Generated Functions
When compiler-generated functions may later need custom behavior, explicitly defining the necessary functions can stabilize the interface.
This avoids a future header change caused solely by switching from implicit to explicit generation.
6.3.7 Removing Include Directives
Remove unnecessary includes from headers.
Use forward declarations when only a type name is required.
Instead of:
#include "widget.h"
class Manager
{
Widget* d_widget_p;
};
prefer:
class Widget;
class Manager
{
Widget* d_widget_p;
};
Then include the full definition in the implementation file.
#include "manager.h"
#include "widget.h"
6.3.8 Removing Default Arguments
Default values can be insulated by avoiding changeable valid defaults directly in the header.
Techniques discussed in the chapter include:
- Using an invalid sentinel default.
- Providing multiple function declarations instead of a changeable default value.
The implementation can then determine the effective default without requiring clients to recompile when it changes.
6.3.9 Removing Enumerations
Ways to reduce coupling from enumerations include:
- Move implementation-only enumerations into the implementation file.
- Replace suitable values with
const staticclass data. - Distribute status values among the classes that actually own them.
Avoid creating one global enumeration that unrelated components must share.
6.4 Total Insulation Techniques
For widely used interfaces, it may be desirable to insulate clients from all implementation details.
The chapter presents three general techniques:
- Protocol Class.
- Fully Insulating Concrete Class.
- Insulating Wrapper Component.
Total Insulation
├── Protocol Class
├── Fully Insulating Concrete Class
└── Insulating Wrapper
6.4.1 The Protocol Class
Protocol Class:
An abstract class representing a pure interface with essentially no implementation.
A protocol class:
- Contains no member data.
- Contains no private or protected members.
- Contains no non-virtual member functions.
- Has a non-inline virtual destructor with an empty implementation.
- Declares all other member functions pure virtual.
- Does not contain implementation details that force clients to depend on a concrete implementation.
Example:
class Shape
{
public:
virtual ~Shape();
virtual void draw() const = 0;
virtual void moveTo(int x, int y) = 0;
};
Principle:
A protocol class is a nearly perfect insulator.
Clients depend on the protocol but not on a particular implementation.
Client
↓
Protocol
↑
Concrete Implementation
This reduces both compile-time and implementation-specific link-time dependency.
6.4.2 The Fully Insulating Concrete Class
A concrete class must still be directly instantiable, so a protocol is not always appropriate.
The chapter's solution is to represent the entire implementation with one opaque pointer.
// example.h
class Example_i;
class Example
{
Example_i* d_this;
public:
Example();
Example(const Example&);
~Example();
Example& operator=(const Example&);
double value() const;
};
Implementation:
// example.c
#include "example.h"
#include "a.h"
#include "b.h"
#include "c.h"
struct Example_i
{
A d_a;
B d_b;
C d_c;
};
Fully Insulating Concrete Class:
A concrete class that:
- Contains exactly one data member: an outwardly opaque pointer to a non-const implementation structure defined in the implementation file.
- Contains no other private or protected members.
- Does not inherit from another class.
- Declares no virtual or inline functions.
Principle:
Holding a single opaque pointer to a structure containing all private members allows a concrete class to insulate its implementation from clients.
Public Object
└── one pointer
↓
Implementation Struct
├── Data
├── Helpers
└── Representation
Principle:
The physical structures of all fully insulating classes appear outwardly identical.
Principle:
A fully insulated implementation can be modified without changing the header.
This technique provides strong insulation but may introduce:
- Dynamic allocation.
- Extra indirection.
- Non-inline function calls.
- Additional implementation complexity.
6.4.3 The Insulating Wrapper
Insulating Wrapper:
A wrapper component that hides lower-level subsystem details from clients and also prevents those details from creating compile-time dependencies.
Client
↓
Insulating Wrapper
↓
Implementation Components
The wrapper becomes the public physical interface of the subsystem.
6.4.3.1 Single-Component Wrappers
A single wrapper component can insulate clients from many lower-level implementation components.
The lower-level objects can continue communicating through their efficient internal interfaces.
Clients interact only with the wrapper.
Clients
↓
Wrapper Component
↓
Internal Hierarchy
6.4.3.2 Multi-Component Wrappers
A wrapper itself can consist of several components when the interface is too large for one component.
However, wrapper components must still obey normal component rules.
Only entities within the appropriate wrapper component can directly access its encapsulated implementation.
Multi-component wrappers therefore require careful planning to avoid long-distance friendship and new physical coupling.
6.5 The Procedural Interface
Procedural Interface:
A collection of functions layered on top of an existing set of components to expose a selected subset of functionality to clients.
It is useful for very large systems that cannot reasonably be redesigned around an insulating C++ wrapper.
External Client
↓
Procedural Interface
↓
Existing C++ System
A procedural interface is:
- Not fully object-oriented.
- Not as logically encapsulating as a wrapper.
- Not necessarily completely insulating.
- Able to provide a stable external interface over a large existing system.
6.5.1 The Procedural Interface Architecture
Procedural-interface functions should reside above the implementation hierarchy.
Each function depends downward on the implementation.
The interface functions should not depend unnecessarily on one another.
Interface Function A ─┐
Interface Function B ─┼─→ Implementation
Interface Function C ─┘
The procedural interface should expose only the functionality required by external clients.
6.5.2 Creating and Destroying Opaque Objects
For an ANSI C-compatible interface:
- Objects are manipulated through pointers.
- Free functions create and destroy objects.
- A consistent registered prefix reduces global-name collisions.
Example:
Stack* pi_createStack();
void pi_destroyStack(Stack* stack);
The actual structure of Stack remains opaque to the procedural client.
Client
└── Stack*
└── Representation hidden
6.5.3 Handles
Handle:
A class that maintains a pointer to an object that is programmatically accessible through the public interface of the handle.
A handle is an object used to refer to another object.
Handle
└── pointer
↓
Managed Object
A handle can help manage dynamically allocated objects.
A manager handle can own the pointed-to object and automatically manage its lifetime.
This reduces direct memory-management responsibility for C++ clients.
6.5.4 Accessing and Manipulating Opaque Objects
Member-function behavior can be exposed through free procedural functions.
The chapter uses a hierarchical naming convention of the general form:
<prefix>_<Subject><Verb><Object>
Examples:
int pi_fStackPop(Stack*);
double pi_fAngleGetDegrees(const Angle*);
Opaque objects are passed through pointers, while free functions perform the requested operation.
The procedural abstraction does not need to reproduce the exact internal class structure.
6.5.5 Inheritance and Opaque Objects
A procedural interface must explicitly represent conversions that C++ inheritance would normally provide automatically.
C++ Interface
Derived* → Base*
automatic conversion
Procedural Interface
↓
Explicit conversion operation
The interface must preserve type safety while allowing clients to access the appropriate base-class view of an opaque object.
6.6 To Insulate or Not to Insulate
Insulation reduces recompilation cost but introduces its own costs.
Possible costs include:
- Dynamic allocation.
- Additional pointer indirection.
- Non-inline function calls.
- Virtual-function calls.
- Additional components.
- More implementation complexity.
- More development effort.
Therefore, not every component should be fully insulated.
6.6.1 The Cost of Insulation
Runtime overhead depends on:
- Function size.
- Function-call frequency.
- Object size.
- Allocation frequency.
- Degree of insulation.
For large functions that already perform substantial work, call overhead may be negligible.
For tiny functions called repeatedly, the overhead of indirection and non-inline calls can dominate.
Fully insulating tiny concrete objects can also introduce expensive dynamic allocation.
Large Operation
└── Insulation overhead relatively small
Tiny Frequent Operation
└── Insulation overhead potentially large
6.6.2 When Not to Insulate
Principle:
A component may reasonably remain uninsulated when it is not widely used.
Light-Weight Component:
A component that, relative to its context:
- Depends on few other components.
- Is inexpensive to construct and destroy.
- Does not allocate additional dynamic memory.
- Makes effective use of inline operations on embedded data.
Principle:
Unless performance is known not to matter, avoid insulating low-level classes with tiny accessor functions that are used heavily.
Examples of lightweight reusable classes may include:
- Point
- Stack
- List
- Small concrete data structures
Principle:
Insulating lightweight, widely used objects commonly returned by value can significantly degrade runtime performance.
Other reasons not to insulate include:
- Few clients.
- Stable implementation.
- Runtime-performance requirements.
- Initial development cost.
- Additional component count.
- Increased implementation complexity.
Principle:
For large, widely used objects, insulate early and selectively remove insulation later if necessary.
6.6.3 How to Insulate
The chapter identifies two main approaches.
1. Extract a Protocol
Use when the abstraction naturally supports a pure interface.
Client
↓
Protocol
↑
Implementation
This provides especially strong insulation because clients need not depend on a particular implementation even at link time.
2. Other Insulation Techniques
Use one or more of:
- Partial insulation.
- Fully insulating concrete class.
- Insulating wrapper.
- Procedural interface.
Need Insulation
├── Protocol Class
└── Other Techniques
├── Partial Insulation
├── Fully Insulating Concrete Class
├── Insulating Wrapper
└── Procedural Interface
Large, high-level, volatile, widely used objects are strong candidates for insulation.
6.6.4 How Much to Insulate
More insulation is not always better.
Principle:
Sometimes total insulation costs no more at runtime than partial insulation.
In other situations:
Principle:
The last small amount of insulation can require a disproportionately large runtime cost.
The chapter's graph experiments show that overhead is especially severe when:
- Tiny wrapper functions are called frequently.
- Small objects require dynamic allocation.
- Small opaque objects are repeatedly returned by value.
- Multiple layers of indirection accumulate.
The correct level of insulation therefore depends on where the insulation boundary is placed.
Too Low
└── Many tiny calls cross insulation boundary
└── High runtime cost
Higher Boundary
└── More work per call
└── Lower relative overhead
Practical Rule
Insulate where:
- The interface is widely used.
- The implementation is likely to change.
- The operations are sufficiently substantial.
- Client recompilation would be expensive.
Reduce or avoid insulation where:
- The component is tiny and stable.
- Inline access is performance-critical.
- Objects are created extremely frequently.
- The insulation layer adds disproportionate runtime cost.
6.7 Key Concepts
Insulation
↓
Reduce Compile-Time Coupling
Encapsulation
└── Hide implementation from programmatic access
Insulation
└── Hide implementation changes from client compilation
Sources of Compile-Time Coupling
├── Inheritance
├── HasA Layering
├── Inline Functions
├── Private Members
├── Protected Members
├── Compiler-Generated Functions
├── Include Directives
├── Default Arguments
└── Enumerations
Partial Insulation
├── WasA → HoldsA
├── HasA → HoldsA
├── Remove Private Helpers from Header
├── Remove Protected Implementation Support
├── Move Private Data Out of Header
├── Stabilize Special Member Declarations
├── Remove Unnecessary Includes
├── Remove Changeable Default Arguments
└── Redistribute Enumerations
Total Insulation
├── Protocol Class
├── Fully Insulating Concrete Class
└── Insulating Wrapper
Procedural Interface
├── Opaque Objects
├── Creation / Destruction Functions
├── Handles
├── Procedural Accessors / Manipulators
└── Explicit Inheritance Conversions
Design Trade-Off
↓
Compile-Time Independence
↕
Runtime / Complexity Cost
Best Candidates for Insulation
├── Widely Used
├── High-Level
├── Large
└── Volatile
Poor Candidates for Full Insulation
├── Tiny
├── Lightweight
├── Stable
└── Performance-Critical