Oddělení modulů - Zrychlení kompilace

Při návrhu objektového API se setkáváme s nutností oddělení jednotlivých modulů tak, aby navzájem spolupracovaly (jeden modul využívá druhý), ale současně změna v impleentaci jednoho modulu nevyvolala nutnost rekompilace modulu druhého. Typickým příkladem je vazba nadřazený-podřazený, ale lze takto vytvořit i cyklickou závislost

Velmi špatným řešením v aplikacích bývá vytvoření globální proměnné.

Řešení lze docílit měkkou vazbou přes ukazatel na nedefinovaný objekt (tzv forward).

Příklad: Máme objekt Controller, který ve svých metodách potřebuje používat funkce z objektů Drive a Sensor. Objekty Drive a Sensor jsou singletony (existují v aplikaci v právě jedné instanci, vytvořené nejspíše jako lokální proměnné ve funkci main.

Forward

Aby mohly jednotlivé metody Controlleru pracovat s Drive, musí mít Controller v sobě uložený odkaz na instanci objektu Drive. Nejspíše takto:

#include "Drive.h"

class Controller {
public:
  Drive *drv;
}

Pokud však s třídou Drive pracuje pouze implementace Controlleru, kompilátor potřebuje pouze vědět, že Drive je třída (a nic víc!) a není tak potřeba vkládat celý soubor s definicí API třídy Drive. Lze toto zjednodušit na:

class Drive;    // pouze forward

class Controller {
public:
  Drive *drv;
}

a soubor s deklarací Drive vkládáme až do cpp souboru

#include "Controller.h"
#include "Drive.h"

Controller:: ......

Toto lze použít při splnění podmínek:

  • Všechny přístupy ke třídě Drive jsou přes ukazatel, nikoliv přímo
    • instance objektu potřebuje znát velikost tedy plnou deklaraci !
  • k obsahu drv se nepřistupuje v headeru ale v kódu cpp.
    • Jakékoliv inline funkce používající obsah drv potřebují znát plnou deklaraci !

Neměnný ukazatel

Dále drive by měl být konstantní ukazatel na nekonstantní instanci třídy Drive. Ukazatel nechceme nikdy měnit, ale vlastní objekt měnit můžeme. S tím kam umístíme const bývá začátečnický problém, pro shrnutí:

Drive *drv;               // ukazatel na Drive. Lze změnit ukazatel i Data na které ukazuje.
const Drive *drv;         // ukazatel na konstantní Drive. Lze změnit ukazatel, Data na které ukazuje lze pouze číst.
Drive * const drv;        // konstantní ukazatel na Drive. Lze změnit data, na které ukazuje. Ukazatel lze pouze číst.
const Drive * const drv;  // konstantní ukazatel na konstantní Drive. Data i ukazatel lze pouze číst.

Dále nechceme, aby do ukazatele v inicializaci instance Controlleru někdo vložil nullptr (nedává to v API smysl, bez instance Drive neumí Controller žít). Navíc nechceme použít pointerovou aritmetiku (z principu se jí vyhýbáme, kde to jde). Místo konstantního ukazatele Drive * const ptr použijeme referenci Drive & ptr. Pozor reference je vždy konstantní, takže nám jedno const odpadá !

Drive & drv;              // reference na Drive. Lze změnit Data na které ukazuje. Ukazatel ne.

Vzorové řešení

Objekt Controller tedy deklarujeme v headeru takto:

class Drive;    // pouze forward

class Controller {
public:
  Controller(Drive & aDrive); 
  
  void Method();
  
private:
  Drive & drv;
}

A implementujeme v cpp souboru takto:

#include "Controller.h"
#include "Drive.h"          // metody controlleru už smí používat API Drive

Controller::Controller(Drive & aDrive)
 : drv{aDrive}
{
} 

void Controller::Method()
{
  drv->CallMe();            // Můžeme používat API
}

Cyklická závislost

Stejným způsobem lze definovat, že Drive může přistupovat k prvkům Controlleru. Standardním způsobem deklarace bysme vytvořili cyklickou závislost mezi headery a kód by (logicky) nešel zkompilovat:

Drive.h:

#include "Controller.h"

class Drive {
 Controller & ctrl;
}

Controller.h:

#include "Drive.h"

class Controller {
 Drive & drv;
}

Řešení cyklické závislosti bylo již uvedeno:

class Controller;

class Drive {
 Controller & ctrl;
}

Controller.h:

class Drive;

class Controller {
 Drive & drv;
}