Чтение онлайн

ЖАНРЫ

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ

Майерс Скотт

Шрифт:

С другой стороны, виртуальные функции связываются динамически (снова см. правило 37), поэтому для них не существует такой проблемы. Если бы функция mf была виртуальной, то ее вызов как посредством pB, так и посредством pD означал бы вызов D::mf, потому в действительности pB и pD указывают на объект типа D.

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

Это

все, что относится к «прагматической» аргументации. Теперь, я уверен, требуется некоторое теоретическое обоснование запрета на переопределение наследуемых невиртуальных функций. С удовольствием его представлю.

В правиле 32 объясняется, что открытое наследование всегда означает «является разновидностью», а в правиле 34 говорится, почему объявление невиртуальной функции в классе определяет инвариант относительно специализации этого класса. Если вы примените эти наблюдения к классам B и D и невиртуальной функции B: mf, то получите следующее:

• Все, что применимо к объектам B, применимо и к объектам D, поскольку каждый объект D также является объектом B;

• Подклассы B должны наследовать как интерфейс, так и реализацию mf, потому что mf невиртуальна в B.

Теперь, если D переопределяет mf, возникает противоречие. Если класс D действительно должен содержать отличную от B реализацию mf и если каждый объект B, являющийся разновидностью B, действительно должен использовать реализацию mf из B, тогда неверно, что каждый объект класса D является разновидностью B. В этом случае D не должен открыто наследовать B. С другой стороны, если класс D действительно должен открыто наследовать B и если D действительно должен содержать реализацию mf, отличную от B, тогда неверно, что mf является инвариантом относительно специализации B. В этом случае mf должна быть виртуальной. И наконец, если каждый объект класса D действительно является разновидностью B и если mf – действительно инвариант относительно специализации B, тогда D, по правде говоря, не нуждается в переопределении mf и не должен пытаться это делать.

Независимо от того, какой из аргументов применим в вашем случае, чем-то придется пожертвовать, но при любых обстоятельствах запрет на переопределение наследуемых невиртуальных функций остается в силе.

Если при чтении этого правила у вас возникло ощущение «дежа вю», то, наверное, вы просто вспомнили правило 7, где я объяснял, почему деструкторы в полиморфных базовых классах должны быть виртуальными. Если вы не следуете этому совету (то есть объявляете невиртуальные деструкторы в полиморфных базовых классах), то нарушаете и требование, изложенное в настоящем правиле, потому что все производные классы автоматически переопределяют унаследованную невиртуальную функцию – деструктор базового класса. Это верно даже для производных классов, в которых нет деструкторов, потому что, как объясняется в правиле 5, компилятор генерирует деструктор автоматически, если вы не определяете его сами. По существу, правило 7 – это лишь частный случай настоящего правила, хотя и заслуживает отдельного внимания и рекомендаций по применению.

Что следует помнить

• Никогда не переопределяйте наследуемые невиртуальные функции.

Правило 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)

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

Поделиться с друзьями: