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

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

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Скотт Майерс, "Эффективное использование 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 из данного класса.

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*