Это имеет прямые следствия для корректного перекрытия функций. Соблюдение отношения включения влечет за собой заменимость — операции, которые применимы ко всему множеству, применимы и к любому его подмножеству. Если базовый класс гарантирует выполнение определенных пред- и постусловий некоторой операции, то и любой производный класс должен обеспечить их выполнение. Перекрытие может предъявлять меньше требований и предоставлять большую функциональность, но никогда не должно предъявлять большие требования или меньше обещать — поскольку это приведет к нарушению контракта с вызывающим кодом.
Определение в производном классе перекрытия, которое может быть неуспешным (например, генерировать исключения; см. рекомендацию 70), будет корректно только в том случае, когда в базовом классе не объявлено, что данная операция всегда успешна. Например, скажем, класс Employee
содержит виртуальную функцию-член GetBuilding
, предназначение которой — вернуть код здания, в котором работает объект Employee
. Но что если мы захотим написать производный класс RemoteContractor
, который перекрывает функцию GetBuilding
, в результате чего она может генерировать исключения или возвращать нулевой код здания? Такое поведение корректно только в том случае, если в документации класса Employee
указано, что функция GetBuilding
может завершаться неуспешно, и в классе RemoteContractor
сообщение о неудаче выполняется документированным в классе Employee
способом.
Никогда не изменяйте аргумент по умолчанию при перекрытии. Он не является частью сигнатуры функции, и клиентский код будет невольно передавать различные аргументы в функцию, в зависимости от того, какой узел иерархии обращается к ней. Рассмотрим следующий пример:
class Base {
// ...
virtual void Foo(int x = 0);
};
class Derived : public Base {
// ...
virtual void Foo(int x = 1); // лучше так не делать...
};
Derived *pD = new Derived;
pD->Foo(); // Вызов pD->Foo(1)
Base *pB = pD;
pB->Foo(); // вызов pB->Foo(0)
У некоторых может вызвать удивление, что одна и та же функция-член одного и того же объекта получает разные аргументы в зависимости от статического типа, посредством которого к ней выполняется обращение.
Желательно добавлять ключевое слово virtual
при перекрытии функций, несмотря на его избыточность — это сделает код более удобным для чтения и понимания.
Не забывайте о том, что перекрытие может скрывать перегруженные функции из базового класса, например:
class Base{ // ...
virtual void Foo(int);
virtual void Foo(int, int);
void Foo(int, int, int);
};
class Derived : public Base { // ...
virtual void Foo(int); // Перекрывает Base::Foo(int),
// скрывая остальные функции
};
Derived d;
d.Foo(1); // Все в порядке
d.Foo(1, 2); // Ошибка
d.Foo(1, 2, 3); // Ошибка
Если перегруженные функции из базового класса должны быть видимы, воспользуйтесь объявлением using
для того, чтобы повторно объявить их в производном классе:
class Derived : public Base { // ...
virtual void Foo(int); // Перекрытие Base::Foo(int)
using Base::Foo; // вносит все прочие перегрузки
// Base::Foo в область видимости
};
Bird
(Птица) определяет виртуальную функцию Fly
и вы порождаете новый класс Ostrich
(известный как птица, которая не летает) из класса Bird
, то как вы реализуете Ostrich::Fly
? Ответ стандартный — "по обстоятельствам". Если Bird::Fly
гарантирует успешность (т.е. обеспечивает гарантию бессбойности; см. рекомендацию 71), поскольку способность летать есть неотъемлемой частью модели Bird
, то класс Ostrich
оказывается неадекватной реализацией такой модели.
39. Виртуальные функции стоит делать неоткрытыми, а открытые — невиртуальными