> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
записать в протокол перед отправкой;
sendClear(info); // если Company == CompanyZ,
// то этой функции не существует
записать в протокол после отправки;
}
...
};
Как следует из комментария, этот код просто не имеет смысла, если базовым классом является MsgSender, так как в нем нет функции sendClear. Поэтому C++ отвергнет такой вызов; компилятор понимает, что шаблон базового класса можно специализировать, и интерфейс, предоставляемый этой специализацией, может быть не таким, как в общем шаблоне. В результате компилятор обычно не ищет унаследованные имена в шаблонных базовых классах. В некотором смысле, когда мы переходим от «объектно-ориентированного C++» к «C++ с шаблонами» (см. правило 1), наследование перестает работать.
Чтобы исправить ситуацию, нужно как-то заставить C++ отказаться от догмы «не заглядывай в шаблонные базовые классы». Добиться этого можно тремя способами. Во-первых, можно предварить обращения к функциям из базового класса указателем this:
template
class LoggingMsgSender: public MsgSender {
public:
...
void sendClearMsg(const MsgInfo& info)
{
записать в протокол перед отправкой
;
this->sendClear(info); // порядок! Предполагается, что
// sendClear будет унаследована
записать в протокол после отправки
;
}
...
};
Во-вторых, можно воспользоваться using-объявлением. Мы уже обсуждали эту тему в правиле 33, где было сказано, что using-объявлением делает скрытые имена из базового класса видимыми в производном классе. Поэтому мы можем переписать sendClearMsg следующим образом:
template
class LoggingMsgSender: public MsgSender {
public:
using MsgSender::sendClear; // сообщает компилятору о том, что
... // sendClear есть в базовом классе
void sendClearMsg(const MsgInfo& info)
{
...
sendClear(info); // нормально, предполагается, что
... // sendClear будет унаследована
}
...
};
Хотя using-объявление будет работать как здесь, так и в правиле 33, но используются они для решения разных задач. Здесь проблема не в том, что имена из базового класса скрыты за именами, объявленными в производном классе, а в том, что компилятор вообще не станет производить поиск в области видимости базового класса, если только вы явно не попросите его об этом.
И последний способ заставить ваш код компилироваться – явно указать, что вызываемая функция находится в базовом классе:
template
class LoggingMsgSender: public MsgSender {
pubilc:
...
void sendClearMsg(const MsgInfo& info)
{
...
MsgSender::sendClear(info); // нормально, предполагается, что
... // sendClear будет унаследована
}
...
};
Но этот способ хуже прочих, посколько если вызываемая функция виртуальна, то явная квалификация отключает динамическое связывание.
С точки зрения видимости имен, все три подхода эквивалентны: они обещают компилятору, что любая специализация шаблона базового класса будет поддерживать интерфейс, предоставленный общим шаблоном. Такое обещание – это все, что необходимо компилятору во время синтаксического анализа производного шаблонного класса, подобного LoggingMsgSender, но если данное обещание не будет выполнено, истина всплывет позже. Например, если в программе есть такой код:
LoggingMsgSender zMsgSender;
MsgInfo msgData;
... // поместить info в msgData
zMsgSender.sendClearMsg(msgData); // ошибка! не скомпилируется
то вызов sendClearMsg не скомпилируется, потому что в этой точке компилятор знает, что базовый класс – это специализация шаблона MsgSender и в нем нет функции sendClear, которую sendClearMsg пытается вызвать.
Таким образом, суть дела в том, когда компилятор диагностирует неправильные обращения к членам базового класса – раньше (когда анализируются определения шаблонов производного класса) или позже (когда эти шаблоны конкретизируются переданными в шаблон аргументами). C++ предпочитает раннюю диагностику, и поэтому предполагает, что о содержимом базовых классов, конкретизируемых из шаблонов, не известно ничего.
Что следует помнить• В шаблонах производных классов ссылки на имена из шаблонов базовых классов осуществляются с помощью префикса «this->», using-объявления либо посредством явного указания базового класса.
Правило 44: Размещайте независимый от параметров код вне шаблонов
Шаблоны – чудесный способ сэкономить время и избежать дублирования кода. Вместо того чтобы вводить код 20 похожих классов, в каждом из которых по 15 функций-членов, вы набираете текст одного шаблона и поручаете компилятору сгенерировать 20 конкретных классов и все 300 необходимых вам функций. (Функции-члены шаблонов классов неявно генерируются, только когда программа к ним обращается, поэтому все 300 функций-членов вы получите, лишь если будете все их использовать.) Шаблоны функций не менее привлекательны. Вместо написания множества однотипных функций вы пишете один шаблон и позволяете компиляторам проделать все остальное. Ну разве не восхитительная технология?
Да… иногда. Если вы не будете внимательны, то использование шаблонов может привести к разбуханию кода. Так называется дублирование в двоичной программе кода, данных или того и другого. В результате компактный и лаконичный исходный код в объектном виде становится громоздким и тяжелым. Хорошего в этом мало, поэтому нужно знать, как избежать такой неприятности.
Основной инструмент для этого – анализ общности и изменчивости имен, но в самой этой идее нет ничего необычного. Даже если вы никогда в жизни не писали шаблонов, таким анализом вам приходится заниматься постоянно.
Когда вы пишете функцию и обнаруживаете, что некоторая часть ее реализации мало чем отличается от реализации другой функции, разве вы дублируете код? Конечно, нет. Вы исключаете общую часть из обеих функций, помещаете ее в третью, а первые две вызывают эту третью функцию. Иными словами, вы анализируете эти две функции на предмет выявления общих и отличающихся частей, перемещаете общие части в новую функцию, а отличающиеся части оставляете на месте. Аналогично, если вы пишете класс и выясняется, что некоторые части этого класса в точности совпадают с частями другого класса, вы не станете их дублировать, а просто вынесете общие части в новый класс, а затем воспользуетесь наследованием или композицией (см. правила 32, 38 и 39), предоставив исходному классу доступ к общим средствам. Отличающиеся части исходных классов остаются на месте.
При написании шаблонов выполняется такой же анализ, и способы борьбы с дублированием аналогичны. Однако имеются новые особенности. В нешаблонном коде дублирование видно сразу: трудно не заметить повторения кода в двух функциях или классах. В шаблонном коде дублирование не бросается в глаза: есть только одна копия исходного кода шаблона, поэтому вам нужно тренироваться, чтобы легко находить места, где в результате конкретизации шаблона может возникнуть дублирование.
Предположим, например, что вы хотите написать шаблон для квадратных матриц фиксированного размера, которые, помимо всего прочего, поддерживают операцию обращения матрицы.
template // шаблон матрицы размерностью n x n,
class SquareMatrix { // состоящей из объектов типа T;
public: // см. ниже информацию о параметре size_t