Скотт Майерс - Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
template <typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
записать в протокол перед отправкой
;
this->sendClear(info); // порядок! Предполагается, что
// sendClear будет унаследована
записать в протокол после отправки
;
}
...
};
Во-вторых, можно воспользоваться using-объявлением. Мы уже обсуждали эту тему в правиле 33, где было сказано, что using-объявлением делает скрытые имена из базового класса видимыми в производном классе. Поэтому мы можем переписать sendClearMsg следующим образом:
template <typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear; // сообщает компилятору о том, что
... // sendClear есть в базовом классе
void sendClearMsg(const MsgInfo& info)
{
...
sendClear(info); // нормально, предполагается, что
... // sendClear будет унаследована
}
...
};
Хотя using-объявление будет работать как здесь, так и в правиле 33, но используются они для решения разных задач. Здесь проблема не в том, что имена из базового класса скрыты за именами, объявленными в производном классе, а в том, что компилятор вообще не станет производить поиск в области видимости базового класса, если только вы явно не попросите его об этом.
И последний способ заставить ваш код компилироваться – явно указать, что вызываемая функция находится в базовом классе:
template <typename Company>
class LoggingMsgSender: public MsgSender<Company> {
pubilc:
...
void sendClearMsg(const MsgInfo& info)
{
...
MsgSender<Company>::sendClear(info); // нормально, предполагается, что
... // sendClear будет унаследована
}
...
};
Но этот способ хуже прочих, посколько если вызываемая функция виртуальна, то явная квалификация отключает динамическое связывание.
С точки зрения видимости имен, все три подхода эквивалентны: они обещают компилятору, что любая специализация шаблона базового класса будет поддерживать интерфейс, предоставленный общим шаблоном. Такое обещание – это все, что необходимо компилятору во время синтаксического анализа производного шаблонного класса, подобного LoggingMsgSender, но если данное обещание не будет выполнено, истина всплывет позже. Например, если в программе есть такой код:
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
... // поместить info в msgData
zMsgSender.sendClearMsg(msgData); // ошибка! не скомпилируется
то вызов sendClearMsg не скомпилируется, потому что в этой точке компилятор знает, что базовый класс – это специализация шаблона MsgSender<CompanyZ> и в нем нет функции sendClear, которую sendClearMsg пытается вызвать.
Таким образом, суть дела в том, когда компилятор диагностирует неправильные обращения к членам базового класса – раньше (когда анализируются определения шаблонов производного класса) или позже (когда эти шаблоны конкретизируются переданными в шаблон аргументами). C++ предпочитает раннюю диагностику, и поэтому предполагает, что о содержимом базовых классов, конкретизируемых из шаблонов, не известно ничего.
Что следует помнить• В шаблонах производных классов ссылки на имена из шаблонов базовых классов осуществляются с помощью префикса «this->», using-объявления либо посредством явного указания базового класса.
Правило 44: Размещайте независимый от параметров код вне шаблонов
Шаблоны – чудесный способ сэкономить время и избежать дублирования кода. Вместо того чтобы вводить код 20 похожих классов, в каждом из которых по 15 функций-членов, вы набираете текст одного шаблона и поручаете компилятору сгенерировать 20 конкретных классов и все 300 необходимых вам функций. (Функции-члены шаблонов классов неявно генерируются, только когда программа к ним обращается, поэтому все 300 функций-членов вы получите, лишь если будете все их использовать.) Шаблоны функций не менее привлекательны. Вместо написания множества однотипных функций вы пишете один шаблон и позволяете компиляторам проделать все остальное. Ну разве не восхитительная технология?
Да… иногда. Если вы не будете внимательны, то использование шаблонов может привести к разбуханию кода. Так называется дублирование в двоичной программе кода, данных или того и другого. В результате компактный и лаконичный исходный код в объектном виде становится громоздким и тяжелым. Хорошего в этом мало, поэтому нужно знать, как избежать такой неприятности.
Основной инструмент для этого – анализ общности и изменчивости имен, но в самой этой идее нет ничего необычного. Даже если вы никогда в жизни не писали шаблонов, таким анализом вам приходится заниматься постоянно.
Когда вы пишете функцию и обнаруживаете, что некоторая часть ее реализации мало чем отличается от реализации другой функции, разве вы дублируете код? Конечно, нет. Вы исключаете общую часть из обеих функций, помещаете ее в третью, а первые две вызывают эту третью функцию. Иными словами, вы анализируете эти две функции на предмет выявления общих и отличающихся частей, перемещаете общие части в новую функцию, а отличающиеся части оставляете на месте. Аналогично, если вы пишете класс и выясняется, что некоторые части этого класса в точности совпадают с частями другого класса, вы не станете их дублировать, а просто вынесете общие части в новый класс, а затем воспользуетесь наследованием или композицией (см. правила 32, 38 и 39), предоставив исходному классу доступ к общим средствам. Отличающиеся части исходных классов остаются на месте.
При написании шаблонов выполняется такой же анализ, и способы борьбы с дублированием аналогичны. Однако имеются новые особенности. В нешаблонном коде дублирование видно сразу: трудно не заметить повторения кода в двух функциях или классах. В шаблонном коде дублирование не бросается в глаза: есть только одна копия исходного кода шаблона, поэтому вам нужно тренироваться, чтобы легко находить места, где в результате конкретизации шаблона может возникнуть дублирование.
Предположим, например, что вы хотите написать шаблон для квадратных матриц фиксированного размера, которые, помимо всего прочего, поддерживают операцию обращения матрицы.
template<typename T, std::size_t n> // шаблон матрицы размерностью n x n,
class SquareMatrix { // состоящей из объектов типа T;
public: // см. ниже информацию о параметре size_t
...
void invert(); // обращение матрицы на месте
};
Этот шаблон принимает параметр типа T, а также параметр типа size_t, не являющийся типом. Параметры, не являющиеся типами, используются реже, чем параметры-типы, но они совершенно законны и, как в данном примере, могут быть вполне естественными.
Теперь рассмотрим такой код:
SquareMatrix<double, 5> sm1;
...
sm1.invert(); // вызов SquareMatrix<double, 5>::invert()
SquareMatrix<double, 10> sm2;
...
sm2.invert(); // вызов SquareMatrix<double, 10>::invert()
Здесь будут конкретизированы две копии функции invert. Они не идентичны, потому что одна из них работает с матрицами 5x5, а другая – с матрицами 10x10, но во всем остальном, кроме констант 5 и 10, эти функции ничем не отличаются. Это – классический пример разбухания кода в результате применения шаблонов.
Что вы делаете, когда есть две функции, абсолютно одинаковые, за исключением того, что в одной используется константа 5, а в другой – 10? Естественно, вы создаете функцию, которая принимает параметр, а затем вызываете ее, один раз передавая в качестве параметра 5, а другой раз – 10. Вот первая попытка проделать тот же трюк в реализации шаблона SquareMatrix:
template<typename T> // базовый класс, не зависящий
class SquareMatrixBase { // от размерности матрицы
protected:
...
void invert(std::size_t matrixSize); // обратить матрицу заданной
... // размерности
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
private:
using SquareMatrixBase<T>::invert; // чтобы избежать сокрытия базовой
// версии invert; см. правило 33
public:
...
void invert() {this->invert(n);} // встроенный вызов версии invert
}; // из базового класса
// см. ниже – почему
// применяется “this->”
Как видите, параметризованная версия функции invert находится в базовом классе – SquareMatrixBase. Как и SquareMatrix, SquareMatrixBase – шаблон, но в отличие от SquareMatrix, он имеет только один параметр – тип объектов в матрице, но не имеет параметра size. Поэтому все матрицы, содержащие объекты заданного типа, будут разделять общий класс SquareMatrixBase. И, значит, все они разделят единственную копию функции invert из данного класса.
Откройте для себя мир чтения на siteknig.com - месте, где каждая книга оживает прямо в браузере. Здесь вас уже ждёт произведение Скотт Майерс - Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ, относящееся к жанру Программирование. Никаких регистраций, никаких преград - только вы и история, доступная в полном формате. Наш литературный портал создан для тех, кто любит комфорт: хотите читать с телефона - пожалуйста; предпочитаете ноутбук - идеально! Все книги открываются моментально и представлены полностью, без сокращений и скрытых страниц. Каталог жанров поможет вам быстро найти что-то по настроению: увлекательный роман, динамичное фэнтези, глубокую классику или лёгкое чтение перед сном. Мы ежедневно расширяем библиотеку, добавляя новые произведения, чтобы вам всегда было что открыть "на потом". Сегодня на siteknig.com доступно более 200000 книг - и каждая готова стать вашей новой любимой. Просто выбирайте, открывайте и наслаждайтесь чтением там, где вам удобно.

