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

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

Предположим, я сообщаю вам, что класс D открыто наследует классу B и что в классе B определена открытая функция-член mf. Ее параметры и тип возвращаемого значения не важны, поэтому давайте просто предположим, что это void. Другими словами, я говорю следующее:

class B {

public:

void mf();

...

};

class D: public B {...};

Даже ничего не зная о B, D или mf, имея объект x типа D,

D x; // x – объект типа D

вы, наверное, удивитесь, когда код

B *pB = &x; // получить указатель на x

PB->mf(); // вызвать mf с помощью ука

поведет себя иначе, чем

D *pD = &x; // получить указатель на x

PD->mf(); // вызвать mf через указатель

Ведь в обоих случаях вы вызываете функцию-член объекта x. Поскольку вы имеете дело с одной и той же функцией и одним и тем же объектом, поведение в обоих случаях должно быть одинаково, не так ли?

Да, так должно быть, но не всегда бывает. В частности, вы получите иной результат, если mf невиртуальна, а D определяет собственную версию mf:

class D: public B {

public:

void mf(); // скрывает B:mf; см. правило 33

...

};

PB->mf(); // вызвать B::mf

PD->mf(); // вызвать D::mf

Причина такого «двуличного» поведения заключается в том, что невиртуальные функции, подобные B::mf и D::mf, связываются статически (см. правило 37). Это означает, что когда pB объявляется как указатель на объект тип B, невиртуальные функции, вызываемые посредством pB, – это всегда функции, определения которых даны в классе B, даже если pB, как в данном примере, указывает на объект класса, производного от B.

С другой стороны, виртуальные функции связываются динамически (снова см. правило 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 – это лишь частный случай настоящего правила, хотя и заслуживает отдельного внимания и рекомендаций по применению.

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

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

Данный текст является ознакомительным фрагментом.