Правило 37: Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию
Правило 37: Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию
Давайте с самого начала упростим обсуждение. Есть только два типа функций, которые можно наследовать: виртуальные и невиртуальные. Но переопределять наследуемые невиртуальные функции в любом случае ошибочно (см. правило 36), поэтому мы вполне можем ограничить наше обсуждение случаем наследования виртуальной функции со значением аргумента по умолчанию.
В этих обстоятельствах мотивировка настоящего правила становится достаточно очевидной: виртуальные функции связываются динамически, а значения аргументов по умолчанию – статически.
Что это значит? Вы говорите, что уже позабыли, в чем заключается разница между статическим и динамическим связыванием? (Кстати, статическое связывание называют еще ранним связыванием, а динамическое – поздним.) Что ж, давайте освежим вашу память.
Статический тип объекта – это тип, объявленный вами в тексте программы. Рассмотрим следующую иерархию классов:
// классы для представления геометрических фигур
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
// все фигуры должны предоставлять функцию для рисования
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle: public Shape {
public:
// заметьте, другое значение параметра по умолчанию – плохо!
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
...
};
Графически это можно представить так:
Теперь рассмотрим следующие указатели:
Shape *ps; // статический тип – Shape*
Shape *pc = new Circle; // статический тип – Shape*
Shape *pr = new Rectangle; // статический тип – Shape*
В этом примере ps, pc и pr объявлены как указатели на Shape, так что для всех них он и будет выступать в роли статического типа. Отметим, что не совершенно безразлично, на что они указывают в действительности, – независимо от этого они имеют статический тип Shape*.
Динамический тип объекта определяется типом того объекта, на который он ссылается в данный момент. Иными словами, динамический тип определяет поведение объекта. В приведенном выше примере динамический тип pc – это Circle*, а динамический тип pr – Recangle*. Что касается ps, то он не имеет динамического типа, потому что не указывает ни на какой объект (пока).
Динамические типы, как следует из их названия, могут изменяться в процессе работы программы, обычно вследствие присваивания:
ps = pc; // динамический тип ps теперь Circle*
ps = pr; // динамический тип ps теперь Rectangle*
Виртуальные функции связываются динамически, то есть динамический тип вызывающего объекта определяет, какая конкретная функция вызывается:
pc->draw(Shape::Red); // вызывается Circle::draw(Shape::Red)
pr->draw(Shape::Red); // вызывается Rectangle::draw(Shape::Red)
Я знаю, что все это давно известно, и вы, несомненно, разбираетесь в виртуальных функциях. Самое интересное начинается, когда мы подходим к виртуальным функциям с аргументами, принимающими значения по умолчанию, поскольку, как я уже сказал, виртуальные функции связываются динамически, а аргументы по умолчанию – статически. Следовательно, вы можете прийти к тому, что будете вызывать виртуальную функцию, определенную в производном классе, но при этом использовать аргументы по умолчанию, заданные в базовом классе:
pr->draw(); // вызывается Rectangle::draw(Shape::Red)!
В этом случае динамический тип pr – это Rectangle*, поэтому, как вы и ожидали, вызывается виртуальная функция класса Rectangle. Для функции Rectangle::draw значение аргумента по умолчанию – Green. Но поскольку статический тип pr – Shape*, то значения аргумента по умолчанию берутся из класса Shape, а не Rectangle! В результате получаем вызов, состоящий из странной, совершенно неожиданной комбинации объявлений draw из классов Shape и Rectangle.
Тот факт, что ps, pc и pr являются указателями, не играет никакой роли. Будь они ссылками, результат остался бы таким же. Важно лишь, что draw – виртуальная функция, и значение по умолчанию одного из ее аргументов переопределено в производном классе.
Почему C++ настаивает на таком диковинном поведении? Ответ на этот вопрос связан с эффективностью исполнения программы. Если бы значения аргументов по умолчанию связывались динамически, то компилятору пришлось бы найти способ во время исполнения определять, какое значение по умолчанию должно быть у параметра виртуальной функции, что медленнее и технически сложнее нынешнего механизма. Решение было принято в пользу скорости и простоты реализации, в результате чего вы можете пользоваться преимуществами эффективного выполнения кода программы. Но если не последуете совету, изложенному в настоящем правиле, то программа будет вести себя нелогично.
Все это прекрасно, но посмотрите, что получится, если, пытаясь следовать этому правилу, вы включите аргументы со значениями по умолчанию в функцию-член, объявленную и в базовом, и в производном классах:
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle: public Shape {
public:
virtual void draw(ShapeColor color = Red) const;
...
};
Гм, дублирование кода! Хуже того: дублирование кода с зависимостями: если значение аргумента по умолчанию изменится в Shape, придется изменить его и во всех производных классах. В противном случае дело закончится переопределением наследуемого значения по умолчанию. Что делать?
Когда у вас возникает проблема с тем, чтобы заставить виртуальную функцию вести себя так, как вы хотите, то благоразумнее рассмотреть альтернативные решения, и в правиле 35 таких альтернатив приведено немало. Одна из них – идиома невиртуального интерфейса (NVI): определить в базовом классе открытую невиртуальную функцию, которая вызывает закрытую виртуальную функцию, переопределяемую в подклассах. В данном случае можно предложить невиртуальную функцию с аргументом по умолчанию и виртуальную функцию, которая выполняет всю реальную работу:
class Shape {
public:
enum ShapeColor( Red, Green, Blue };
void draw(ShapeColor color = Red) const // теперь – невиртуальная
{
doDraw(color); // вызов виртуальной функции
}
...
private:
virtual void doDraw(ShapeColor color) const = 0; // реальная работа
}; // выполняется
// в этой функции
class Rectangle: public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const // обратите внимание
... // на отсутствие у аргумента
}; // значения по умолчанию
Поскольку невиртуальные функции никогда не должны переопределяться в производных классах (см. правило 36), то ясно, что при таком подходе значение по умолчанию для параметра color функции draw всегда будет Red.
Что следует помнить
• Никогда не переопределяйте наследуемые значения аргументов по умолчанию, потому что аргументы по умолчанию связываются статически, тогда как виртуальные функции – а только их и можно переопределять, – динамически.
Данный текст является ознакомительным фрагментом.