Insulation
Insulation is the process of avoiding or removing unnecessary compile-time coupling.
Insulation is a physical design issue whereas encapsulation refers to logical design.
If a component can be modified without forcing clients to recompile is said to be insulated.
The following sections list techniques to minimize excessive compile-time dependencies:
Remove Private Inheritance
Private Inheritance exposes some but not all functions in private base class to clients of derived class.
// base.h #ifndef _INCLUDED_BASE #define _INCLUDED_BASE class Base { public: Base() {} ~Base() {} void f1(int); int f1() const; }; #endif//_INCLUDED_BASE |
// myclass.h #ifndef _INCLUDED_MYCLASS #define _INCLUDED_MYCLASS #ifndef _INCLUDED_BASE #include "base.h" #endif//_INCLUDED_BASE class MyClass : private Base { public: MyClass() {} Base::f1; // access declaration }; #endif//_INCLUDED_MYCLASS |
However, the same logical interface can be achieved without exposing clients to details of the class.
// myclass.h #ifndef _INCLUDED_MYCLASS #define _INCLUDED_MYCLASS class Base; class MyClass { public: MyClass(); MyClass(const MyClass& c); ~MyClass(); MyClass& operator=(const MyClass& c); void f1(int i); int f1() const; private: Base* p_base; }; #endif//_INCLUDED_MYCLASS |
Instead of privately deriving from Base, an implementation now holds an opaque pointer to Base;
New member functions of MyClass are defined (out-of-line) to forward calls to functions in Base.
// my_class.c #include "myclass.h" #include "base.h" MyClass::MyClass() : p_base(new Base) {} MyClass::MyClass(const MyClass& c) : p_base(new Base(*c.p_base)) {} MyClass::~MyClass() { delete p_base; } MyClass& MyClass::operator=(const MyClass& c) { if (this != &c) { delete p_base; p_base = new Base(*c.p_base); } return *this; } void MyClass::f1(int i) { p_base->f1(i); } int MyClass::f1() const { return p_base->f1(); } |
Remove Embedded Data Members
Insulate clients from an individual implementation class:
- Convert all embedded instances of that implementation class to pointers (or references)
- Manage those pointers explicitly in constructors, destructors and assignment operators
That is, convert HasA relationship to HoldsA to improve insulation.
Here, compare before insulating clients of class to after insulation.
// myclass_before.h #ifndef _INCLUDED_MYCLASS_BEFORE #define _INCLUDED_MYCLASS_BEFORE #ifndef _INCLUDED_YOURCLASS #include "yourclass.h" #endif//_INCLUDED_YOURCLASS class MyClass { public: MyClass(); ~MyClass(); int getValue() const; int getCount() const { return count; } private: YourClass yours; int count; }; #endif//_INCLUDED_MYCLASS_BEFORE // my_class_before.c #include "myclass_before.h" MyClass::MyClass() { } MyClass::~MyClass() { } int MyClass::getValue() const { return yours.getValue(); } |
// myclass_after.h #ifndef _INCLUDED_MYCLASS_AFTER #define _INCLUDED_MYCLASS_AFTER class YourClass; class MyClass { public: MyClass(); ~MyClass(); int getValue() const; int getCount() const { return count; } private: YourClass* p_yours; int count; }; #endif//_INCLUDED_MYCLASS_AFTER // my_class_after.c #include "myclass_after.h" #include "yourclass.h" MyClass::MyClass() { p_yours = new YourClass; } MyClass::~MyClass() { delete p_yours; } int MyClass::getValue() const { return p_yours->getValue(); } |
Remove Private Member Functions
Private member functions, although encapsulated, are part of the component's physical interface.
Instead of making functions private, make them static free (non-member) declared at file scope.
Compare an original class, modified with only private static functions then with static free functions:
// myclass_before.h #ifndef _INCLUDED_MYCLASS_BEFORE #define _INCLUDED_MYCLASS_BEFORE class MyClass { public: int getValue() const; private: int value() const; }; #endif//_INCLUDED_MYCLASS_BEFORE |
Step One: convert each private member function to a private static member:
// myclass_after1.h #ifndef _INCLUDED_MYCLASS_AFTER1 #define _INCLUDED_MYCLASS_AFTER1 class MyClass { public: int getValue() const; private: static int value(const MyClass&); }; #endif//_INCLUDED_MYCLASS_AFTER1 // myclass_after1.c #include "myclass_after1.h" int MyClass::getValue() const { return value(*this); } int MyClass::value(const MyClass& myclass) { return 0; } |
Step Two: remove function declaration from header file and member notation from definition file:
// myclass_after2.h #ifndef _INCLUDED_MYCLASS_AFTER2 #define _INCLUDED_MYCLASS_AFTER2 class MyClass { public: int getValue() const; }; #endif//_INCLUDED_MYCLASS_AFTER2 // myclass_after2.c #include "myclass_after2.h" int MyClass::getValue() const { return value(*this); } static int value(const MyClass& myClass) { return 0; } |
Remove Private Member Data
Removing private static data is easy for inline member functions that do not require direct access:
Move static member data into a static variable defined at file scope in the component's .c file.
Compare original class with private static member data and modified class with static file-scope data:
// myclass_before.h #ifndef _INCLUDED_MYCLASS_BEFORE #define _INCLUDED_MYCLASS_BEFORE class MyClass { public: private: static int s_count; }; #endif//_INCLUDED_MYCLASS_BEFORE // my_class_before.c #include "myclass_before.h" int MyClass::s_count; |
// myclass_after.h #ifndef _INCLUDED_MYCLASS_AFTER #define _INCLUDED_MYCLASS_AFTER class MyClass { public: private: }; #endif//_INCLUDED_MYCLASS_AFTER // my_class_after.c #include "myclass_after.h" static int s_count; |
Remove Compiler-Generated Functions
The compiler will auto-generate the following functions if not found: constructor, copy constructor, assignment operator and/or destructor. However, truly insulating class must define these explicitly.
Remove Include Directives
Unnecessary #include directives can cause compile-time coupling where none would otherwise exist: Move all unnecessary #include directives from a header file to .c file with forward class declarations.
// bank.h class USD; // class declaration instead of #include class CAD; // class declaration instead of #include class NZD; // class declaration instead of #include class Bank { public: USD getUSD() const; CAD getCAD() const; NZD getNZD() const; }; |
An alternative is to place redundant guards around each #include directive in every header file.
Although unpleasant, this may significantly reduce compile times in much larger C++ projects.
// bank.h #ifndef _INCLUDED_BANK #define _INCLUDED_BANK #ifndef _INCLUDED_USD #include "usd.h" #endif//_INCLUDED_USD #ifndef _INCLUDED_CAD #include "cad.h" #endif//_INCLUDED_CAD #ifndef _INCLUDED_NZD #include "nzd.h" #endif//_INCLUDED_NZD class Bank {}; #endif//_INCLUDED_BANK |
Remove Enumerations
Judicious use of enumerations, typedefs, + other constructs with internal linkage achieve good insulation: Move enumerations and typedefs to .c file and replace them as private static const member of the class.
Summary
Generally if a component is used widely throughout the system, its interface should be insulated.
However, insulated interfaces are not always practical, e.g. lightweight, reusuable components.
Techniques
Remove Private Inheritance | Convert WasA relationship to HoldsA |
Remove Embedded Data Members | Convert HasA relationship to HoldsA |
Remove Private Member Functions | Make them static at file scope and move into .c file |
Remove Private Member Data | Extract a protocol and/or move static data to .c file |
Remove Compiler Functions | Explicitly define these functions |
Remove Include Directives | Remove include directives or replace with class declarations |
Remove Enumerations | Relocate to .c file and replace as const static class member data |
Conclusion
In conclusion, two important techniques can be used to mitigate cyclic dependencies in C++: Levelization can reduce link-time dependencies and Insulation can minimize compile-time dependencies. Awesome J