Отметим, что функция, отправляющая сообщение, в производном классе называется иначе (sendClearMsg), чем в базовом (sendClear). Это хорошее решение, потому что таким образом мы обходим проблему сокрытия унаследованных имен (см. правило 33), а равно сложности, возникающие при переопределении наследуемых невиртуальных функций (см. правило 36). Но этот код не будет компилироваться, по крайней мере, компилятором, совместимым со стандартом. Такой компилятор решит, что функции sendClear не существует. Мы видим, что эта функция определена в базовом классе, но компилятор не станет искать ее там. Попытаемся понять – почему.
Проблема в том, что когда компилятор встречает определение шаблона класса LoggingMsgSender, он не знает, какому классу тот наследует. Понятно, что классу MsgSender, но Company – параметр шаблона, который не известен до момента конкретизации LoggingMsgSender. Не зная, что такое Company, невозможно понять, как выглядит класс MsgSender. В частности, не существует способа узнать, есть ли в нем функция sendClear.
Чтобы яснее почувствовать, в чем сложность, предположим, что у нас есть класс CompanyZ, описывающий компанию, которая настаивает на том, чтобы все сообщения шифровались:
class CompanyZ { // этот класс не представляет
public: // функции sendCleartext
...
void sendEncrypted(const std::string& msg);
...
};
Общий шаблон MsgSender не подходит для CompanyZ, потому что в нем определена функция sendClear, которая для объектов класса CompanyZ не имеет смысла. Чтобы решить эту проблему, мы можем создать специализированную версию MsgSender для CompanyZ:
template <> // полная специализация MsgSender;
class MsgSender { // отличается от общего шаблона
public: // только отсутствием функции
... // sendCleartext
void sendSecret(const MsgInfo& info)
{...}
};
Обратите внимание на синтаксическую конструкцию «template<>» в начале определения класса. Она означает, что это и не шаблон, и не автономный класс. Это специализированная версия шаблона MsgSender, которая должна использоваться, если параметром шаблона является CompanyZ. Называется это полной специализацией шаблона : шаблон MsgSender специализирован для типа CompanyZ, и эта специализация применяется, коль скоро в качестве параметра указан тип CompanyZ, никакие другие особенности параметров шаблона во внимание не принимаются.
Имея специализацию шаблона MsgSender для CompanyZ, снова рассмотрим производный класс LoggingMsgSender:
template
class LoggingMsgSender: public MsgSender {
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); // нормально, предполагается, что
Читать дальше
Конец ознакомительного отрывка
Купить книгу