Главная страница

Голуб Ален И. - Веревка достаточной длины, чтобы... выстрелить с. Руководство по программированию. Автору удается сделать изложение столь серьезной темы живым и интересным за счет рассыпанного по тексту юмора и глубокого знания предмета


Скачать 1.36 Mb.
НазваниеРуководство по программированию. Автору удается сделать изложение столь серьезной темы живым и интересным за счет рассыпанного по тексту юмора и глубокого знания предмета
Дата12.06.2019
Размер1.36 Mb.
Формат файлаpdf
Имя файлаГолуб Ален И. - Веревка достаточной длины, чтобы... выстрелить с.pdf
ТипРуководство
#81349
страница16 из 17
1   ...   9   10   11   12   13   14   15   16   17

Часть 8з. Шаблоны
Многие проблемы с шаблонами в действительности вызваны учебниками, которые обычно настолько упрощенно рассматривают шаблоны, что вы заканчиваете чтение, не получив и намека на то, как они должны использоваться. Этот раздел посвящен распространенным затруднениям, связанным с шаблонами.
155. Используйте встроенные шаблоны функций
вместо параметризированных макросов
Приведенный ранее пример:
#define
SQUARE(x) ((x) * (x)) где:
SQUARE(++x) расширяется до:
((++x)*(++x)) инкрементируя x дважды. Вы не можете решить эту проблему в Си, а в
Си++ можете. Простая встроенная функция работает вполне удовлетворительно, в таком виде:
inline
int
square(
int
x ){
return
x * x; } не давая побочного эффекта. Тем не менее, она допускает лишь целочисленные аргументы. Шаблон функции, который расширяется во множество перегруженных встроенных функций, является более общим решением:
template

inline
type square( type x ){
return
x * x; }
К несчастью, это срабатывает только в простых ситуациях. Следующий шаблон не может обработать вызов max(10, 10L)
, потому что не совпадают типы аргументов:
template

inline
type max( type x, type y ){
return
(x > y) ? x : y; }
Для обработки max(10, 10L)
вы должны использовать прототип, чтобы принудить к расширению по тому варианту max()
, который может выполнить данную работу:

Шаблоны
221
long
max(
long
,
long
);
Прототип вызывает расширение шаблона. Компилятор с легкостью преобразует аргумент типа
int
в
long
, даже если ему не нужно делать это преобразование для расширения шаблона.
Заметьте, что я здесь рекомендую использование шаблонов только потому, что square является встроенной функцией. Если бы этого не было, то для того, чтобы такой механизм был жизнеспособным, пришлось бы генерировать слишком много кода.
156. Всегда знайте размер шаблона после его
расширения
Большинство книг демонстрирует шаблоны типа простого контейнера массива, подобного показаному на листинге 13. Вы не можете использовать здесь наследование (скажем, с базовым классом array, от которого наследуется int_array
). Проблема заключается в перегрузке операции
operator
[]()
. Вы бы хотели, чтобы она была виртуальной функцией в базовом классе, замещенная затем в производном классе, но сигнатура версии производного класса должна отличаться от сигнатуры базового класса, чтобы все это заработало. Здесь определения функций должны отличаться лишь возвращаемыми типами: int_array::
operator
[]()
должна возвращать ссылку на тип
int
, а long_array::
operator
[]()
должна возвращать ссылку на тип
long
, и так далее. Так как время возврата не рассматривается как часть сигнатуры при выборе перегруженной функции, то реализация на основе наследования не жизнеспособна. Единственным решением является шаблон.
Листинг 13. Простой контейнер массива
1
template
<
class
type,
int
size >
2
class
array
3 {
4 type array[size];
5
public
:
6
class
out_of_bounds {}; // возбуждается исключение, если
7 // индекс за пределами массива
8 type &
operator
[](
int
index);
9 };
10 11
template
<
class
type,
int
size >
12
inline
type &array::
operator
[](
int
index)
13 {
14
if
( 0 <= index && index < size )
15
return
array[ index ]

Правила программирования на Си++
222 16 throw out_of_bounds;
17 }
Единственная причина осуществимости этого определения заключается в том, что функция-член является встроенной. Если бы этого не было, то вы могли бы получить значительное количество повторяющегося кода.
Запомните, что везде далее происходит полное расширение шаблона, включая все функции-члены

. Вследствие того, что каждое из следующих определений на самом деле создает разный тип, то вы должны расширить этот шаблон четыре раза, генерируя четыре идентичные функции
operator
[]()
, по одной для каждого расширения шаблона: array<
int
,10> ten_element_array; array<
int
,11> eleven_element_array; array<
int
,12> twelve_element_array; array<
int
,13> thirteen_element_array;
(то есть array<
int
,10>::
operator
[]()
, array<
int
,11>::
operator
[]()
и так далее).
Вопрос состоит в том, как сократить до минимума дублирование кода.
Что, если мы уберем размер за пределы шаблона, как на листинге 14?
Предыдущие объявления теперь выглядят так: array<
int
> ten_element_array (10); array<
int
> eleven_element_array (11); array<
int
> twelve_element_array (12); array<
int
> thirteen_element_array (13);
Теперь у нас есть только одно определение класса (и один вариант
operator
[]()
) с четырьмя объектами этого класса.
Листинг 14. Шаблон массива (второй проход)
1
template
<
class
type>
2
class
array
3 {
4 type *array;
5
int
size;
6
public
:
7
virtual

array(
void
);
8 array(
int
size = 128 );
9 10
class
out_of_bounds {}; // возбуждается исключение, если
11 // индекс за пределами массива
12 type &
operator
[](
int
index);
13 };

Функции из шаблонов генерируется, только если они используются в программе (по крайней мере, так должен поступать хороший компилятор). — Ред.

Шаблоны
223 14 15
template
<
class
type>
16 array::array(
int
sz /*= 128*/ ): size(sz)
17 , array( new type[ sz ] )
18 {}
19 20
template
<
class
type>
21 array::array(
void
)
22 {
23
delete
[] array;
24 }
25 26
template
<
class
type>
27
inline
type &array::
operator
[](
int
index)
28 {
29
if
( 0 <= index && index < size )
30
return
array[ index ]
31 throw out_of_bounds;
32 }
Главным недостатком этой второй реализации является то, что вы не можете объявить двухмерный массив. Определение на листинге 13 разрешает следующее: array< array<
int
, 10>, 20> ar;
(20-элементный массив из 10-элементных массивов). Определение на листинге 14 устанавливает размер массива, используя конструктор, поэтому лучшее, что вы можете получить, это: array< array<
int
> > ar2(20);
Внутренний array<
int
>
создан с использованием конструктора по умолчанию, поэтому это 128-элементный массив; мы объявили 20- элементный массив из 128-элементных массивов.
Вы можете решить эту последнюю проблему при помощи наследования. Рассмотрим следующее определение производного класса:
template
<
class
type,
int
size >
class
sized_array :
public
array
{
public
: sized_array() : array(size) {}
};
Здесь ничего нет, кроме единственной встроенной функции, поэтому это определение очень маленького класса. Оно совсем не будет увеличивать размер программы, вне зависимости от того, сколько раз будет расширен шаблон. Вы теперь можете записать: sized_array< sized_array<
int
,10>, 20> ar3;

Правила программирования на Си++
224 для того, чтобы получить 20-элементный массив из 10-элементных массивов.
157. Шаблоны классов должны обычно определять
производные классы
158. Шаблоны не заменяют наследование; они его
автоматизируют
Главное, что нужно запомнить о шаблонах классов, — это то, что они порождают много определений классов. Как и всякий раз, когда у вас есть множество сходных определений классов, идентичные функции должны быть соединены в общий базовый класс.
Во-первых, давайте взглянем на то, что не нужно делать. Класс storable
, уже использованный мной, снова представляется хорошим примером. Сначала создадим объект collection для управления сохраняемыми объектами:
class
collection
{ storable *head;
public
:
// ... storable *find( const storable &a_match_of_this ) const;
}; storable *collection::find(
const
storable &a_match_of_this )
const
{
// Послать сообщение объекту начала списка, указывающее, что спи–
// сок просматривается на совпадение со значением a_match_of_this;
return
head ? head->find( a_match_of_this )
: NULL
;
}
Механизм поиска нужных объектов скрыт внутри класса storable
. Вы можете изменить лежащую в основе структуру данных, поменяв определение storable
, и эти изменения совсем не затронут реализацию класса collection
Затем давайте реализуем класс storable
, использующий простой связанный список в качестве лежащей в основе структуры данных:
class
storable
{ storable *next, *prev;

Шаблоны
225
public
: storable *find (
const
storable &match_of_this )
const
; storable *successor (
void
)
const
;
virtual
int
operator
== (
const
storable &r )
const
;
}; storable *storable::find(
const
storable &match_of_this )
const
{
// Возвращает указатель на первый элемент в списке (начиная с
// себя), имеющий тот же ключ, что и match_of_this. Обычно,
// объект-коллекция должен послать это сообщение объекту начала
// списка, указатель на который хранится в классе коллекции. storable *current =
this
;
for
( ; current; current = current->next )
if
( *current == match_of_this ) // найдено совпадение
return
current;
} storable *storable::successor(
void
)
const
{
// Возвращает следующее значение в последовательности.
return
next;
}
Функция
operator
==()
должна быть чисто виртуальной, потому что отсутствует возможность ее реализации на уровне класса storable
Реализация должна быть выполнена в производном классе
13
:
class
storable_string :
public
storable
{ string s;
public
:
virtual
int
operator
==(
const
storable &r )
const
;
// ...
};
virtual
int
operator
==(
const
storable &r )
const
{ storable_string *right = dynamic_cast( &r
);
return
right ? (s == r.s) : NULL;
}
Я здесь использовал предложенный в ISO/ANSI Cи++ безопасный механизм нисходящего приведения типов. right инициализируется значением
NULL
, если передаваемый объект (
r
) не относится к типу
13
В действительности я бы использовал множественное наследование с участием класса string. Использованный здесь код имеет цель немного упростить пример.

Правила программирования на Си++
226 storable_string
. Например, он может принадлежать к некоторому другому классу, также являющемуся наследником storable
Пока все идет хорошо. Теперь к проблемам, связанным с шаблонами.
Кто-нибудь, не понимающий того, что делает, говорит: "Ребята, я могу исключить наследование и потребность в виртуальных функциях, используя шаблоны", а делает, вероятно, нечто подобное:
template
<
class
t_key>
class
storable
{ storable *next, *prev; t_key key;
public
:
// ... storable *find (
const
storable &match_me )
const
; storable *successor (
void
)
const
;
int
operator
==(
const
storable &r )
const
;
};
template
<
class
t_key>
int
storable::
operator
==(
const
storable &r )
const
{
return
key == r.key ;
}
template
<
class
t_key> storable *storable::successor(
void
)
const
{
return
next;
}
template
<
class
t_key> storable *storable::find(
const
storable
&match_me )
const
{ storable *current =
this
;
for
( ; current; current = current->next )
if
( *current == match_me ) // найдено совпадение
return
current;
}
Проблема здесь в непроизводительных затратах. Функции-члены шаблона класса сами являются шаблонами функций. Когда компилятор расширяет шаблон storable
, он также расширяет варианты всех функций-членов этого шаблона

. Хотя я их не показал, вероятно, в классе storable

См. предûдущее примечание к правилу 156. — Ред.

Шаблоны
227 определено множество функций. Многие из этих функций будут похожи в том, что они не используют информацию о типе, передаваемую в шаблон. Это означает, что каждое расширение такой функции будет идентично по содержанию любому другому ее расширению. Из функций, которые не похожи на функцию successor()
, большинство будут подобны find()
, использующей информацию о типе, но которую легко изменить так, чтобы ее не использовать.
Вы можете решить эту проблему, используя механизм шаблонов для создания производного класса. Основываясь на предыдущей реализации, не использующей шаблоны, вы можете сделать следующее:
template
<
class
t_key>
class
storable_tem :
public
storable
{ t_key key;
public
:
// Замещение базового класса virtual
int
operator
==(
const
storable &r )
const
;
// ...
};
template
<
class
t_key>
/* виртуальный */
int
storable_tem::
operator
==(
const
storable &r )
const
{ t_key *right = dynamic_cast( &r );
return
right ? (s == r.s) : NULL;
}
Выбрав другой путь, я сосредоточил в базовом классе все функции, которые не зависят от типа key
. Затем я использовал механизм шаблонов для создания определения производного класса, реализующего только те функции, которым нужно знать тип key
Полезным результатом является существенное сокращение размера кода. Механизм шаблонов может рассматриваться как средство автоматизации производства шаблонных производных классов.

Правила программирования на Си++
228
Часть 8и. Исключения
159. Назначение исключений — не быть пойманными
Как правило, исключение должно быть возбуждено, если:

Нет другого способа сообщить об ошибке (например, конструкторов, перегруженных операций и т.д.).

Ошибка неисправимая (например, нехватка памяти).

Ошибка настолько непонятная или неожиданная, что никому не придет в голову ее протестировать (например, printf
).
Исключения были включены в язык для обработки ошибочных ситуаций, которые иначе не могут быть обработаны, таких, как ошибка, случающаяся в конструкторе или перегруженной операции. Без использования исключений единственным способом обнаружения ошибки в конструкторе будет передача этому объекту сообщения: some_obj x;
if
( x.is_invalid() )
// конструктор не выполнился. что, по меньшей мере, неаккуратно. Перегруженные операции являют собой ту же проблему. Единственным способом, которым использованная в x = a + b; функция
operator
+()
может сообщить об ошибке, является возврат неверного значения, которое будет скопировано в x
. Вы могли бы затем написать:
if
( x == INVALID )
// ... или нечто подобное. Снова весьма неаккуратно. Исключения также полезны для обработки ошибок, которые обычно являются фатальными.
Например, большинство программ просто вызовут exit()
, если функция malloc()
не выполнится. Все проверки типа:
if
( !(p = malloc(size)) ) fatal_error( E_NO_MEMORY ); бесполезны, если оператор
new
просто не возвратит значения, когда ему не хватит памяти. Так как
new
на самом деле возбуждает исключение (по сравнению с вызовом exit()
), то вы можете перехватить это

Исключения
229 исключение в тех редких случаях, когда вы можете что-то сделать в такой ситуации.
Также имеется и другая проблема. Одной из причин того, что комитет
ISO/ANSI по Си++ требует, чтобы оператор
new
возбуждал исключение, если он не может выделить память, заключается в том, что кто-то провел исследование и обнаружил, что какая-то смехотворная доля ошибок времени выполнения в реальных программах вызвана людьми, не побеспокоившимися проверить, не вернула ли функция malloc()
значение
NULL
. По причинам, обсуждаемым позже, я не думаю, что исключение должно быть использовано вместо возврата ошибки просто для защиты программистов от себя самих, но оно срабатывает с
new
, потому что эта ошибка обычно в любом случае неисправима. Лучшим примером может быть функция printf()
. Большинство программистов на Си даже не знают, что printf()
возвращает код ошибки. (Она возвращает количество выведенных символов, которое может быть равно
0
, если на диске нет места). Программисты, которые не знают о возврате ошибки, склонны ее игнорировать. А это не очень хорошо для программы, которая осуществляет запись в перенаправленный стандартный вывод, продолжать, как будто все в порядке, поэтому можно считать хорошей идеей возбудить здесь исключение.
Итак, что же плохого в исключениях? На самом деле существует две проблемы. Первой является читаемость. Вам будет тяжело меня убедить, что: some_class obj;
1   ...   9   10   11   12   13   14   15   16   17


написать администратору сайта