Friday, March 17, 2017

Large Scale C++ Projects II

In the previous post, we discussed Levelization to help reduce link-time dependencies in Large Scale C++ Projects. Now let's build here to discuss Insulation techniques to help reduce compile-time dependencies.

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