Категории
Самые читаемые
Лучшие книги » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

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

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

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 51 52 53 54 55 56 57 58 59 ... 73
Перейти на страницу:

}; // членов: typename необходимо

Такая несогласованность несколько раздражает, но по мере приобретения опыта вы перестанете ее замечать.

Рассмотрим еще один пример использования typename, потому нечто подобное можно встретить в реальном коде. Предположим, что мы пишем шаблон функции, которая принимает итератор, и хотим сделать локальную копию – temp – объекта, на который этот итератор указывает. Это можно сделать примерно так:

template <typename IterT>

void workWithIterator(IterT iter)

{

typename std::iterator_traits<IterT>::value_type temp(*iter);

...

}

Не пугайтесь при виде выражения std::iterator_traits<IterT>::value_type. Здесь просто используются стандартные классы-характеристики (traits) (см. правило 47). Так, на C++ говорят «тип того, на что указывает объект типа *IterT». В этом предложении объявлена локальная переменная (temp) того же типа, что и объекты, на которые указывает IterT, а затем она инициализирована значением, на которое указывает iter. Если IterT будет типа vector<int>::iterator, то temp будет иметь тип int. Если же IterT будет типа vector<string>::iterator, то temp будет иметь тип string. Поскольку std::iterator_traits<IterT>::value_type – это вложенное зависимое имя типа (value_type вложено внутрь iterator_traits<IterT>, а IterT – параметр шаблона), мы должны предварить его словом typename.

Если вам неприятно даже видеть выражение std::iterator_traits<IterT>::value_type, представьте, каково набирать его на клавиатуре. Если вы, как и большинство программистов, считаете, что набрать такое более одного раза немыслимо, определите псевдоним для этого типа посредством typedef. Для имен членов классов-характеристик, к каковым относится value_type, (см. в правиле 47 информацию о классах-характеристиках), принято соглашение, согласно которому имя typedef должно совпадать с именем члена. Таким образом, определение локального typedef обычно выглядит так:

template <typename IterT>

void workWithIterator(IterT iter)

{

typedef typename std::iterator_traits<IterT>::value_type value_type;

value_type temp(*iter);

...

}

Многих программистов соседство typedef и typename поначалу раздражает, но это логическое следствие из правила обращения к вложенным зависимым именам типов. Вы скоро привыкнете. К тому же у вас есть на то веские причины. Сколько раз вы готовы напечатать std::iterator_traits<IterT>::value_type?

В качестве заключительного замечания я должен упомянуть, что не все компиляторы настаивают на строгом выполнении правил, касающихся ключевого слова typename. Некоторые принимают код, в котором typename требуется, но пропущено; некоторые принимают код, где typename присутствует, но не допускается; и некоторые (обычно это касается старых компиляторов) отвергают typename даже там, где оно необходимо. Это значит, что взаимосвязи между typename и вложенными зависимыми имен типов могут стать причиной некоторых не очень серьезных ошибок при переносе программ на другую платформу.

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

• В объявлениях параметров шаблона ключевые слова class и typename взаимозаменяемы.

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

Правило 43: Необходимо знать, как обращаться к именам в шаблонных базовых классах

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

class CompanyA {

public:

...

void sendClearText(const std::string& msg);

void sendEncryptedText(const std::string& msg);

...

};

class CompanyB{

public:

...

void sendClearText(const std::string& msg);

void sendEncryptedText(const std::string& msg);

...

};

... // классы для других компаний

class MsgInfo {...}; // класс, содержащий информацию,

// используемую для создания

// сообщения

template<typename Company>

class MsgSender {

public:

... // конструктор, деструктор и т. п.

void sendClear(const MsgInfo& info)

{

std::string msg;

создать msg из info

Company c;

c.sendClearText(msg);

}

void sendSecret(const MsgInfo& info) // аналогично sendClear, но вызывает

{...} // c.sendEncrypted

};

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

template <typename Company>

class LoggingMsgSender: public MsgSender<Company> {

public:

...

void sendClearMsg(const MsgInfo& info)

{

записать в протокол перед отправкой;

sendClear(info); // вызвать функцию из базового класса

// этот код не будет компилироваться!

записать в протокол после отправки;

}

...

};

Отметим, что функция, отправляющая сообщение, в производном классе называется иначе (sendClearMsg), чем в базовом (sendClear). Это хорошее решение, потому что таким образом мы обходим проблему сокрытия унаследованных имен (см. правило 33), а равно сложности, возникающие при переопределении наследуемых невиртуальных функций (см. правило 36). Но этот код не будет компилироваться, по крайней мере, компилятором, совместимым со стандартом. Такой компилятор решит, что функции sendClear не существует. Мы видим, что эта функция определена в базовом классе, но компилятор не станет искать ее там. Попытаемся понять – почему.

Проблема в том, что когда компилятор встречает определение шаблона класса LoggingMsgSender, он не знает, какому классу тот наследует. Понятно, что классу MsgSender<Company>, но Company – параметр шаблона, который не известен до момента конкретизации LoggingMsgSender. Не зная, что такое Company, невозможно понять, как выглядит класс MsgSender<Company>. В частности, не существует способа узнать, есть ли в нем функция sendClear.

Чтобы яснее почувствовать, в чем сложность, предположим, что у нас есть класс CompanyZ, описывающий компанию, которая настаивает на том, чтобы все сообщения шифровались:

class CompanyZ { // этот класс не представляет

public: // функции sendCleartext

...

void sendEncrypted(const std::string& msg);

...

};

Общий шаблон MsgSender не подходит для CompanyZ, потому что в нем определена функция sendClear, которая для объектов класса CompanyZ не имеет смысла. Чтобы решить эту проблему, мы можем создать специализированную версию MsgSender для CompanyZ:

template <> // полная специализация MsgSender;

class MsgSender <CompanyZ> { // отличается от общего шаблона

public: // только отсутствием функции

... // sendCleartext

void sendSecret(const MsgInfo& info)

{...}

};

Обратите внимание на синтаксическую конструкцию «template<>» в начале определения класса. Она означает, что это и не шаблон, и не автономный класс. Это специализированная версия шаблона MsgSender, которая должна использоваться, если параметром шаблона является CompanyZ. Называется это полной специализацией шаблона : шаблон MsgSender специализирован для типа CompanyZ, и эта специализация применяется, коль скоро в качестве параметра указан тип CompanyZ, никакие другие особенности параметров шаблона во внимание не принимаются.

Имея специализацию шаблона MsgSender для CompanyZ, снова рассмотрим производный класс LoggingMsgSender:

template <typename Company>

class LoggingMsgSender: public MsgSender<Company> {

public:

...

void sendClearMsg(const MsgInfo& info)

{

записать в протокол перед отправкой;

sendClear(info); // если Company == CompanyZ,

// то этой функции не существует

записать в протокол после отправки;

}

...

};

Как следует из комментария, этот код просто не имеет смысла, если базовым классом является MsgSender<CompanyZ>, так как в нем нет функции sendClear. Поэтому C++ отвергнет такой вызов; компилятор понимает, что шаблон базового класса можно специализировать, и интерфейс, предоставляемый этой специализацией, может быть не таким, как в общем шаблоне. В результате компилятор обычно не ищет унаследованные имена в шаблонных базовых классах. В некотором смысле, когда мы переходим от «объектно-ориентированного C++» к «C++ с шаблонами» (см. правило 1), наследование перестает работать.

Чтобы исправить ситуацию, нужно как-то заставить C++ отказаться от догмы «не заглядывай в шаблонные базовые классы». Добиться этого можно тремя способами. Во-первых, можно предварить обращения к функциям из базового класса указателем this:

1 ... 51 52 53 54 55 56 57 58 59 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии