Bool operator< ( intvalue ) const

ООП: Лекция 6. Шаблоны. Основы обобщенного программирования.

Версия 3.01 21 августа 2016г.

(С) 2013-2016, Зайченко Сергей Александрович, к.т.н, ХНУРЭ, доцент кафедры АПВТ

 

Введение в шаблоны - шаблоны функций

 

Достаточно часто в программах встречается необходимость решать одни и те же задачи для различных типов данных. Яркий пример - семейство из 3 похожих функций для взятия абсолютного значения числа в математической библиотеке языка C:

intabs ( int)

longlabs ( long)

doublefabs ( double)

 

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

 

intabs ( int_value )

{

return( _value < 0 ) ? - value : value;

}

 

longlabs ( long_value )

{

return( _value < 0 ) ? - value : value;

}

 

doublefabs ( double_value )

{

return( _value < 0 ) ? - value : value;

}

 

Только лишь из-за разницы типов аргументов в языке С пришлось вводить 3 функции с похожими, но отличающимися названиями. В языке С++ был добавлен механизм перегрузки функций (т.е., наличия функций с одинаковым названием и различными типами аргументов), позволяющий, по крайней мере, унифицировать название функции, что удобно для использующего кода:

 

intabs ( int_value )

{

return( _value < 0 ) ? - value : value;

}

 

longabs ( long_value )

{

return( _value < 0 ) ? - value : value;

}

 

doubleabs ( double_value )

{

return( _value < 0 ) ? - value : value;

}

 

Все же, это не отменяет необходимость в написании одного и того же кода несколько раз с банальной заменой типов аргументов. Этот процесс для программиста, как для думающего человека, визуально неприятен, несмотря на изобретение комбинации клавиш Ctrl-C + Ctrl-V в текстовых редакторах. Увидев такую дубликацию алгоритма, программист должен, как минимум, насторожиться и поискать лучшее решение.

 

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

 

#define ABS_VALUE( _v ) ( ( ( _v ) < 0 ) ? - ( _v ) : ( _v ) )

 

Функционально макрос решает поставленную задачу. Однако, помимо не слишком читабельного внешнего вида, препроцессор привносит целый букет нежелательных технических проблем:

● макрос является низкоуровневой текстовой подстановкой, а значит:

○ сложно понять ошибки компиляции, если в теле есть ошибки;

○ макрос крайне неудобно отлаживать в пошаговом режиме в среде разработки;

○ отсутствуют какие-либо проверки типов аргументов в принципе, поскольку препроцессор ничего не знает ни о системе типов языка программирования, ни об областях видимости;

● поскольку в качестве аргумента могут передаваться выражения, нужно крайне аккуратно расставлять скобки при каждом использовании аргумента макроса в теле, иначе это может привести к нарушению приоритета операторов;

● в отличие от функции, будучи примененным к выражению, макрос развернется в блок кода, который вычислит это выражение несколько раз:

○ это может быть неэффективным с точки зрения производительности (можно понадеяться на оптимизацию кода при компиляции, но без 100% гарантии)

 

ABS_VALUE( a + b )
=>
( ( ( a + b ) < 0 ) ? - ( a + b ) : ( a + b ) )

○ это может привести к некорректным результатам, если выражение содержит побочные эффекты:

 

ABS_VALUE( ++a )
=>
( ( ( ++a ) < 0 ) ? - ( ++a ) : ( ++a ) )

 

Намного лучшим решением являются ШАБЛОНЫ (templates) - функции и классы, описывающие некоторое обобщенное поведение для одного или нескольких не заданных заранее типов данных. Конкретные типы данных передаются шаблонами при использовании, формируя окончательный вариант реализации. Программист задает обобщенное описание-шаблон и использует его с конкретными типами, а задачу генерации реального кода реализации шаблона для конкретного указанного типа данных на себя полностью берет компилятор.

 

Шаблоны являются одной из форм повторного использования в языке С++, наряду с композицией объектов и наследованием классов. Их синтаксис и правила использования существенно сложнее, и такой код компилируется значительно дольше обычного. На основе шаблонов построена существенная часть стандартной библиотеки языка С++ - библиотека STL (Standard Template Library - стандартная библиотека шаблонов), реализующая типовые структуры данных (контейнеры), часто используемые алгоритмы и другие полезные обобщенные абстракции.

 

Стиль программирования, в основе которого лежат шаблоны и их взаимодействие, называется ОБОБЩЕННЫМ ПРОГРАММИРОВАНИЕМ (generic programming). Обобщенное программирование в С++ гармонично переплетается с объектно-ориентированным стилем.

 

Запишем алгоритм вычисления абсолютного значения в виде шаблона функции:

 

template< typenameT >

Tabs ( T _value )

{

return( _value < 0 ) ? - value : value;

}

 

Ключевое слово template означает, что далее записывается шаблон. В угловых скобках указываются аргументы шаблона - декларация typenameT означает, что описание будет производиться относительно неизвестного на данном этапе типа данных T. В дальнейшем это имя может использоваться как в прототипе так и в теле шаблона.

 

В состав шаблонов функций входит два наборами аргументов:

● аргументы шаблона (typename T);

● аргументы функции (T _value).

 

Аргументы шаблона используются для подстановки конкретных типов, а аргументы функции - в обычных целях, для передачи данных.

 

Вместо ключевого слова typename можно использовать слово class. С точки зрения поведения в данном контексте эти ключевые слова играют идентичную роль. Ключевое слово typename является предпочтительным из стилистических соображений, поскольку по смыслу вместо аргумента T могут подставляться не только классы, но и встроенные типы, такие как int. Учитывая возможность подстановки встроенных типов, ключевое слово class может сбивать с толку, хотя и не окажет никакого влияния на функциональность.

 

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

● копировать (передача аргумента функции типа T по значению и возврат из функции);

● сравнивать с целым числом 0 при помощи оператора <;

● применять оператор унарного минуса.

 

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

 

structMoney

{

intm_dollars, m_cents;

 

Money ( int_dollars, int_cents )

: m_dollars( _dollars ), m_cents( _cents )

{

}

 

bool operator< ( intvalue ) const

{

returnm_dollars < value;

}

 

Money operator- () const

{

returnMoney( - m_dollars, m_cents );

}

};

 

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

 

Второй этап компиляции шаблона выполняется при подстановке конкретных типов вместо аргументов. Его называют ИНСТАНЦИРОВАНИЕМ (instantiation) шаблона. Фактический аргумент для типа T можно указать явно:

 

std::cout << abs< double>( -2.5 );

std::cout << abs< int>( -2 );

std::cout << abs< Money>( Money( -5, 20 ) ).m_dollars;

 

либо для простых случаев он будет автоматически выведен (deduced) из контекста по переданным аргументам:

 

std::cout << abs( -2.5 );

std::cout << abs( -2 );

std::cout << abs( Money( -5, 20 ) ).m_dollars;

 

Для каждого использования шаблона с уникальным фактическим аргументом компилятор в результате инстанцирования сгенерирует отдельную копию с подставленными вместо T конкретным типом. При этом выполнятся все ранее отложенные компилятором проверки, ожидавшие информации о конкретном типе T.

 

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

 

Набор ошибок, проверяемых на первом и втором этапах компиляции шаблонов существенно различается между конкретными компиляторами. В частности, компилятор компании Microsoft на первом этапе выполняет минимальный набор синтаксических проверок, таких как наличие нужного количества пар открывающих-закрывающих фигурных скобок, и т.п. примитивных проверок. Большинство проверок правил языка откладывается до 2 этапа. Он спокойно отнесется к такому забавному коду:

 

template< typenameT >

voidf ( T _val )

{

Привет студентам факультета КИУ!

}

 

а испускать сообщения об ошибках компиляции начнет только в случае инстанцирования:

 

f( 3 );

 

error C2065: 'Привет' : undeclared identifier

 

Другие компиляторы, в частности GCC, могут пресекать настолько некорректный код уже на первом этапе компиляции, оставляя на второй этап только те проверки, которые действительно нельзя осуществить без наличия информации о конкретном использованном типе.