Разнесение объявления и определения класса

В крупных программных проектах удобно разделять классы на краткое описание (объявление) и определение, включающее основные детали реализации. В некоторых случаях такое разделение становится необходимостью, если классы тем или иными образом ссылаются друг друга, то есть ни один не может быть полностью определен без предварительных объявлений.

Мы видели пример предварительного объявления в разделе Указатели (см. файл ThisCallback.mq5), где классы Manager и Element содержат взаимные указатели. Там было сделано предварительное объявление класса в краткой форме: в виде заголовка с ключевым словом class и именем:

class Manager;

Однако это — минимально возможное объявление. Оно регистрирует только имя и дает возможность отложить описание программного интерфейса до некоторых пор, но где-то позднее в коде это описание обязательно должно встретиться.

Более часто объявление включает в себя полное описание интерфейса: в нем указываются все переменные и заголовки методов класса, но без их тел (блоков кода).

Определения методов записываются отдельно: с заголовками, в которых используются полностью квалифицированные имена, включающие имя класса (или нескольких классов и пространств имен, если контекст метода имеет большой уровень вложенности). Имена всех классов и имя метода соединяются с помощью оператора выделения контекста '::'.

тип имя_класса [:: имя_вложенного_класса ...] :: имя_метода([параметры...])
{
}

В принципе, можно часть методов определить непосредственно в блоке описания класса (обычно так делают с малыми функциями), а часть — вынести отдельно (как правило, крупные функции). Но метод должен иметь только одно определение (то есть нельзя определить метод в блоке класса, и затем — еще раз отдельно) и одно объявление (определение в блоке класса является и объявлением).

Перечень параметров, возвращаемый тип, модификаторы const (если есть) должны полностью совпадать в объявлении и определении метода.

Посмотрим, как можно разнести описание и определение классов из скрипта ThisCallback.mq5 (пример из раздела Указатели): создадим его аналог с именем ThisCallback2.mq5.

В начале по-прежнему будет идти предварительное объявление Manager. Далее оба класса Element и Manager объявлены без реализации: вместо блока кода с телом метода стоит точка с запятой.

class Manager// предварительное объявление
  
class Element
{
   Manager *owner// указатель
public:
   Element(Manager &t);
   void doMath();
   string getMyName() const;
};
  
class Manager
{
   Element *elements[1]; // массив указателей (заменить на динамический)
public:
   ~Manager();
   Element *addElement();
   void progressNotify(Element *econst float percent);
};

Во второй части исходного кода представлены реализации всех методов (сами реализации — без изменений).

Element::Element(Manager &t) : owner(&t)
{
}
 
void Element::doMath()
{
   ...
}
 
string Element::getMyName() const
{
   return typename(this);
}
 
Manager::~Manager()
{
   ...
}
 
Element *Manager::addElement()
{
   ...
}
 
void Manager::progressNotify(Element *econst float percent)
{
   ...
}

Раздельное объявление и определение методов поддерживают также и структуры.

Обратите внимание, что список инициализации конструктора (после имени и ':') является частью определения и потому должен предшествовать телу функции (иными словами, список инициализации недопустим в объявлении конструктора, где присутствует только заголовок).

Раздельное написание объявления и определения позволяет разрабатывать библиотеки, исходный код которых должен быть закрытым. В этом случае объявления располагают в отдельном заголовочном файле с расширением mqh, а определения — в одноименном файле с расширением mq5. Программу компилируют и распространяют в виде ex5-файла, к которому прикладывается заголовочный файл с описанием внешнего интерфейса.

При этом может возникнуть вопрос, почему часть внутренней реализации, в частности организация данных (переменные), видна во внешнем интерфейсе. Строго говоря, это сигнализирует о недостаточном уровне абстракции в иерархии классов. Все классы, предоставляющие внешний интерфейс, не должны раскрывать никаких деталей реализации.

Иными словами, если бы мы ставили своей целью экспортировать вышеприведенные классы из некоей библиотеки, то нужно было бы выделить их методы в базовые классы, которые предоставили бы описание API (без полей с данными), а Manager и Element унаследовать от них. При этом в методах базовых классов мы не можем использовать никаких данных производных классов и, по большому счету, они вообще не могут иметь реализации. Как такое возможно?

Для этого существует технология абстрактных методов, абстрактных классов и интерфейсов.