Управление наследованием: final и delete

MQL5 позволяет накладывать некоторые ограничения на наследование классов и структур.

 

Ключевое слово final

С помощью ключевого слова final, добавленного после имени класса, разработчик может запретить наследование от этого класса. Например (FinalDelete.mq5):

class Base
{
};
 
class Derived final : public Base
{
};
 
class Concrete : public Derived // ОШИБКА
{
};

Компилятор выдаст ошибку "нельзя наследоваться от 'Derived', потому что он определен как 'final'" ("cannot inherit from 'Derived' as it has been declared as 'final'").

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

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

Дополнительным преимуществом от использования final может быть оптимизация кода компилятором: для указателей "финальных" типов он сможет заменить динамическую диспетчеризацию виртуальных функций на статическую.

 

Ключевое слово delete

Ключевое слово delete может быть указано в заголовке метода, чтобы сделать его недоступным в текущем классе и его наследниках. Виртуальные методы родительских классов удалять таким образом нельзя (это нарушило бы "контракт" класса, то есть наследники перестали бы "являться" ("is a") представителями того же рода).

class Base
{
public:
   void method() { Print(__FUNCSIG__); }
};
 
class Derived : public Base
{
public:
   void method() = delete;
};
 
void OnStart()
{
   Base *b;
   Derived d;
   
   b = &d;
   b.method();
   
   // ОШИБКА:   
   // attempting to reference deleted function 'void Derived::method()'
   //    function 'void Derived::method()' was explicitly deleted
   d.method();
}

Попытка его вызвать будет приводить к ошибке компиляции.

Мы видели похожую ошибку в разделе Приведение объектных типов, потому что компилятор обладает некоторым интеллектом, чтобы также "удалять" методы в определенных условиях.

Помечать удаленными рекомендуется следующие методы, для которых компилятор предоставляет неявные реализации:

  • конструктор по умолчанию: Class(void) = delete;
  • конструктор копирования: Class(const Class &object) = delete;
  • оператор копирования/присваивания: void operator=(const Class &object) = delete.

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