Поскольку в C++ однопроходный компилятор, и для увязки единиц компиляции используются заголовочные файлы, содержащие объявление всех нижеиспользуемых абстрактных типов данных и функций, в C++ есть некоторые специфические приёмы программирования.
Одним из них является PImpl (pointer to implementation) - способ и скрыть от пользователя особенности реализации того или иного класса, и, что гораздо важнее, хоть как-то упорядочить декларационные зависимости между элементами, не перегружая заголовочный файл и пространство имён всякими "потрохами" классов.
Идея предельно простая:
В заголовочном файле, допустим, header.h, пишем
#ifndef HEADER_H
#define HEADER_H
class C
{
private:
class Implementation;
Implementation *implementation;
public:
C();
void f();
~C();
};
#endif
здесь только продекларировано существование класса Implementation, и поскольку нет обращений к его членам, а есть только указатель (размер которого в памяти не зависит от типа), компилятор это успешно переваривает.
В файле реализации, допустим, code.cpp, пишем
#include<iostream>
#include "header.h"
using namespace std;
class C::Implementation
{
public:
void f()
{
wcout << L"Hello world" << endl;
}
};
C::C() :
implementation(NULL)
{
this->implementation = new C::Implementation();
}
void C::f()
{
this->implementation->f();
}
C::~C()
{
delete this->implementation
}
Разумеется, преимущества, связанные с полной инкапсуляцией реализации класса от пользователя, оборачиваются недостатком обязательного создания объекта в куче и рисками утечек памяти при некорректном завершении жизни объекта, а также дополнительными вызовами обёрточных функций. Ну и ко всему прочему приходится писать дополнительный обёрточный код, который всегда увеличивает трудозатраты на внесение изменений (по сравнению с Java и C#).
В вызывающем коде класс C выглядит и используется обычным образом
#include "header.h"
int main()
{
C c;
c.f(); // Hello world.
return 0;
}
Положение становится скверным, когда класс является параметризируемым - шаблоном (template) в терминах C++. Код такого класса не компилируется до подстановки параметров, и для разных параметров компилятор генерирует разные реализации параметризированного класса. По этой причине весь код реализации шаблона обязан находиться в заголовочном файле, чтобы компилятор в момент определения всех параметров мог сгенерировать новый класс по шаблону (для старых стандартов до C++11).
#ifndef HEADER_H
#define HEADER_H
#include <iostream>
#include <string>
using namespace std;
template<typename T>
class C
{
public:
void Process(T &item)
{
wcout << item << endl;
}
};
#endif
На первый взгляд это с PImpl несовместимо. Однако в C++ есть ещё и специализации шаблонов: заранее определённые программистом версии параметризированного класса с конкретными значениями параметров.
template<>
class C<wstring>
{
public:
void Process(wstring &item)
{
wcout << item << endl;
}
};
Поэтому в том частном случае, когда набор значений параметров шаблонов заранее известен, можно добиться комбинации шаблонов и PImpl.
Заголовочный файл header.h
#ifndef HEADER_H
#define HEADER_H
#include <cstdlib>
template<typename T>
class Class
{
private:
class Implementation;
Implementation *implementation;
public:
Class();
void Process(T &item);
~Class();
};
#endif
Здесь мы определяем шаблон класса C, но реализация его методов будет специализированной под конкретные типы, и поэтому может быть вынесена из заголовочного файла. Для всех типов, не входящих в перечень специализаций, будет возникать ошибка компиляции. Внутри определяем класс Implementation, чтобы на не него распространялось действие параметра T, поскольку Implementation - тоже элемент специализации. Определение снаружи потребует template-выражения.
Файл реализации code.cpp
#include <iostream>
#include <string>
#include "header.h"
using namespace std;
template<typename T>
class Class<T>::Implementation
{
public:
void Process(T &item)
{
wcout << item << endl;
}
};
#define SPECIALIZATION(T) \
\
Class<T>::Class() : \
implementation(NULL) \
{ \
this->implementation = new Class<T>::Implementation(); \
} \
\
void Class<T>::Process(T &item) \
{ \
this->implementation->Process(item); \
} \
\
Class<T>::~Class() \
{ \
delete this->implementation; \
} \
SPECIALIZATION(wstring)
SPECIALIZATION(int)
SPECIALIZATION(double)
#undef SPECIALIZATION
Здесь мы вынуждены реализовать все варианты каждого метода класса C, чтобы получились специализации для каждого типа. Чтобы не заниматься copy-paste вручную, разумно использовать макрос. А класс Implementation мы можем оформить в виде шаблона, вызываемого из каждой специализации - для каждой специализации будет автоматически создан собственный вариант.
Код вызывающей программы по-прежнему прост и обыкновенен:
#include <iostream>
#include <string>
#include "header.h"
using namespace std;
int main()
{
{
wstring x(L"Hello world");
Class<wstring> object;
object.Process(x);
}
{
double x = 3.14159;
Class<double> object;
object.Process(x);
}
/* Тут будет ошибка - нет специализации для float.
{
float x = 3.14159;
Class<float> object;
object.Process(x);
}
*/
wcin.get();
return 0;
}