Эффективное использование 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), поэтому мы вполне можем ограничить наше
В этих обстоятельствах мотивировка настоящего правила становится достаточно очевидной: виртуальные функции связываются динамически, а значения аргументов по умолчанию – статически.
Что это значит? Вы говорите, что уже позабыли, в чем заключается разница между статическим и динамическим связыванием? (Кстати, статическое связывание называют еще ранним связыванием, а динамическое – поздним.) Что ж, давайте освежим вашу память.
Статический тип объекта – это тип, объявленный вами в тексте программы. Рассмотрим следующую иерархию классов:
Графически это можно представить так:
Теперь рассмотрим следующие указатели:
В этом примере ps, pc и pr объявлены как указатели на Shape, так что для всех них он и будет выступать в роли статического типа. Отметим, что не совершенно безразлично, на что они указывают в действительности, – независимо от этого они имеют статический тип Shape*.
Динамический тип объекта определяется типом того объекта, на который он ссылается в данный момент. Иными словами, динамический тип определяет поведение объекта. В приведенном выше примере динамический тип pc – это Circle*, а динамический тип pr – Recangle*. Что касается ps, то он не имеет динамического типа, потому что не указывает ни на какой объект (пока).
Динамические типы, как следует из их названия, могут изменяться в процессе работы программы, обычно вследствие присваивания:
Виртуальные функции связываются динамически, то есть динамический тип вызывающего объекта определяет, какая конкретная функция вызывается: