НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
Что лучше?
Всего ответов: 323
Оbserver Ward

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

12. Обобщенные алгоритмы
12. Обобщенные алгоритмы

В нашу реализацию класса Array (см. главу 2) мы включили функции-члены для поддержки операций min(), max() и sort(). Однако в стандартном классе vector эти, на первый взгляд фундаментальные, операции отсутствуют. Для нахождения минимального или максимального значения элементов вектора следует вызвать один из обобщенных алгоритмов. Алгоритмами они называются потому, что реализуют такие распространенные операции, как min(), max(), find() и sort(), а обобщенными (generic) – потому, что применимы к различным контейнерным типам: векторам, спискам, массивам. Контейнер связывается с применяемым к нему обобщенным алгоритмом посредством пары итераторов (мы говорили о них в разделе 6.5), указывающих, какие элементы следует посетить при обходе контейнера. Специальные объекты-функции позволяют переопределить семантику операторов в обобщенных алгоритмах. Итак, в этой главе рассматриваются обобщенные алгоритмы, объекты-функции и итераторы.
12.1. Краткий обзор

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

   1. По очереди исследовать каждый элемент.
   2. Если элемент равен искомому значению, то вернуть его позицию в коллекции.
   3. В противном случае анализировать следующий элемент Повторять шаг 2, пока значение не будет найдено либо пока не будет просмотрена вся коллекция.
   4. Если мы достигли конца коллекции и не нашли искомого, то вернуть некоторое значение, показывающее, что нужного элемента нет.

Алгоритм, как мы и утверждали, не зависит ни от типа контейнера, к которому применяется, ни от типа искомого значения, однако для его использования необходимы:

    * способ обхода коллекции: переход к следующему элементу и распознавание того, что достигнут конец коллекции. При работе с встроенным типом массива мы решаем эту проблему, передавая два аргумента: указатель на первый элемент и число элементов, подлежащих обходу (в случае строк символов в стиле C передавать второй аргумент необязательно, так как конец строки обозначается двоичным нулем);
    * умение сравнивать каждый элемент контейнера с искомым значением. Обычно это делается с помощью оператора равенства, ассоциированного со значениями типа, или путем передачи указателя на функцию, осуществляющую сравнение;
    * некоторый обобщенный тип для представления позиции элемента внутри контейнера и специального признака на случай, если элемент не найден. Обычно мы возвращаем индекс элемента либо указатель на него. В ситуации, когда поиск неудачен, возвращается –1 вместо индекса или 0 вместо указателя.

Обобщенные алгоритмы решают первую проблему, обход контейнера, с помощью абстракции итератора – обобщенного указателя, поддерживающего оператор инкремента для доступа к следующему элементу, оператор разыменования для получения его значения и операторы равенства и неравенства для определения того, совпадают ли два итератора. Диапазон, к которому применяется алгоритм, помечается парой итераторов: first адресует первый элемент, а last – тот, который следует за последним. К самому элементу, адресованному итератором last, алгоритм не применяется; он служит стражем, прекращающим обход. Кроме того, last используется как возвращаемое значение с семантикой "отсутствует”. Если же значение получено, то возвращается итератор, помечающий позицию найденного элемента.
Имеется по две версии каждого обобщенного алгоритма: в одной для сравнения применяется оператор равенства, а в другой – объект-функция или указатель на функцию, реализующую сравнение. (Объекты-функции рассматриваются в разделе 12.3.) Вот, например, реализация обобщенного алгоритма find(), в котором используется оператор сравнения для типов хранимых в контейнере элементов:

template < class ForwardIterator, class Type >
ForwardIterator
find( ForwardIterator first, ForwardIterator last, Type value )
{
   for ( ; first != last; ++first )
         if ( value == *first )
          return first;
   return last;
}

ForwardIterator (однонаправленный итератор) – это один из пяти категорий итераторов, предопределенных в стандартной библиотеке. Он поддерживает чтение и запись адресуемого элемента. (Все пять категорий рассматриваются в разделе 12.4.)
Алгоритмы достигают независимости от типов за счет того, что никогда не обращаются к элементам контейнера непосредственно; доступ и обход элементов осуществляются только с помощью итераторов. Неизвестны ни фактический тип контейнера, ни даже то, является ли он контейнером или встроенным массивом. Для работы со встроенным типом массива обобщенному алгоритму можно передать не только обычные указатели, но и итераторы. Например, алгоритм find() для встроенного массива элементов типа int можно использовать так:

#include <algoritm>
#include <iostream>

int main()
{
   int search_value;
   int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };

   cout << "enter search value: ";
   cin >> search_value;

   int *presult = find( &ia[0], &ia[6], search_value );

   cout  << "The value " << search_value
        << ( presult == &ia[6]
            ? " is not present" : " is present" )
   << endl;
}


Если возвращенный указатель равен адресу &ia[6] (который расположен за последним элементом массива), то поиск оказался безрезультатным, в противном случае значение найдено.
Вообще говоря, при передаче адресов элементов массива обобщенному алгоритму мы можем написать

int *presult = find( &ia[0], &ia[6], search_value );

или

int *presult = find( ia, ia+6, search_value );

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

// искать только среди элементов ia[1] и ia[2]
int *presult = find( &ia[1], &ia[3], search_value );

А вот пример использования контейнера типа vector с алгоритмом find():

#include <algorithm>
#include <vector>
#include <iostream>

int main()
{
   int search_value;
   int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };
   vector<int> vec( ia, ia+6 );

   cout << "enter search value:  ";
   cin >> search_value;

   vector<int>::iterator presult;
   presult = find( vec.begin(), vec.end(), search_value );

   cout << "The value  " << search_value
        <<( presult == vec.end()
            ?  " is not present " :  " is present" )
        << endl;
}

find() можно применить и к списку:

#include <algorithm>
#include <list>
#include <iostream>
int main()
{
   int search_value;
   int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };
   list<int> ilist( ia, ia+6 );

   cout << "enter search value: ";
   cin >> search_value;

   list<int>::iterator presult;
   presult = find( ilist.begin(), ilist.end(), search_value );

   cout << "The value "<<search_value
      <<( presult == ilist.end()
            ? " is not present" : " is present" )
 << endl;
}

(В следующем разделе мы обсудим построение программы, в которой используются различные обобщенные алгоритмы, а затем рассмотрим объекты-функции. В разделе 12.4 мы подробнее расскажем об итераторах. Развернутое введение в обобщенные алгоритмы – предмет раздела 12.5, а их детальное обсуждение и иллюстрация применения вынесено в Приложение. В конце главы речь пойдет о случаях, когда применение обобщенных алгоритмов неуместно.)
Упражнение 12.1
Обобщенные алгоритмы критикуют за то, что при всей элегантности дизайна проверка корректности возлагается на программиста. Например, если передан неверный итератор или пара итераторов, помечающая неверный диапазон, то поведение программы не определено. Вы согласны с такой критикой? Следует ли оставить применение обобщенных алгоритмов только наиболее квалифицированным специалистам? Может быть, нужно запретить использование потенциально опасных конструкций, таких, как обобщенные алгоритмы, указатели и явные приведения типов?
12.2. Использование обобщенных алгоритмов

Допустим, мы задумали написать книжку для детей и хотим понять, какой словарный состав наиболее подходит для такой цели. Чтобы ответить на этот вопрос, нужно прочитать несколько детских книг, сохранить текст в отдельных векторах строк (см. раздел 6.7) и подвергнуть его следующей обработке:

   1. Создать копию каждого вектора.
   2. Слить все векторы в один.
   3. Отсортировать его в алфавитном порядке.
   4. Удалить все дубликаты.
   5. Снова отсортировать, но уже по длине слов.
   6. Подсчитать число слов, длина которых больше шести знаков (предполагается, что длина – это некоторая мера сложности, по крайней мере, в терминах словаря).
   7. Удалить семантически нейтральные слова (например, союзы and (и), if (если), or (или), but (но) и т.д.).
   8. Напечатать получившийся вектор.

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

#include <vector>
#include <string>

typedef vector<string, allocator> textwords;
void process_vocab( vector<textwords, allocator> *pvec )
{
   if ( ! pvec ) {
      // выдать предупредительное сообщение
      return;
   }

   // ...
}

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

#include <algorithm>
#include <iterator>

void process_vocab( vector<textwords, allocator> *pvec )
{
   // ...
   vector< string > texts;

   vector<textwords, allocator>::iterator iter = pvec->begin();
   for ( ; iter != pvec->end(); ++iter )
      copy( (*iter).begin(), (*iter).end(), back_inserter( texts ));

   // ...
}

Первыми двумя аргументами алгоритма copy() являются итераторы, ограничивающие диапазон подлежащих копированию элементов. Третий аргумент – это итератор, указывающий на место, куда надо копировать элементы. back_inserter называется адаптером итератора; он позволяет вставлять элементы в конец вектора, переданного ему в качестве аргумента. (Подробнее мы рассмотрим адаптеры итераторов в разделе 12.4.).
Алгоритм unique() удаляет из контейнера дубликаты, расположенные рядом. Если дана последовательность 01123211, то результатом будет 012321, а не 0123. Чтобы получить вторую последовательность, необходимо сначала отсортировать вектор с помощью алгоритма sort(); тогда из последовательности 01111223 получится 0123. (Хотя на самом деле получится 01231223.)
unique() не изменяет размер контейнера. Вместо этого каждый уникальный элемент помещается в очередную свободную позицию, начиная с первой. В нашем примере физический результат – это последовательность 01231223; остаток 1223 – это, так сказать, " отходы" алгоритма. unique() возвращает итератор, указывающий на начало этого остатка. Как правило, этот итератор затем передается алгоритму erase() для удаления ненужных элементов. (Поскольку встроенный массив не поддерживает операции erase(), то семейство алгоритмов unique() в меньшей степени подходит для работы с ним.) Вот соответствующий фрагмент функции:

void process_vocab( vector *pvec )
{
   // ...
   // отсортировать вектор texts
   sort( texts.begin(), texts.end() );

   // удалить дубликаты
   vector<string, allocator>::iterator it;
   it = unique( texts.begin(), texts.end() );
   texts.erase( it, texts.end() );

   // ...
}

Ниже приведен результат печати вектора texts, объединяющего два небольших текстовых файла, после применения sort(), но до применения unique():

a a a a alice alive almost
alternately ancient and and and and and and
and as asks at at beautiful becomes bird
bird blows blue bounded but by calling coat
daddy daddy daddy dark darkened darkening distant each
either emma eternity falls fear fiery fiery flight
flowing for grow hair hair has he heaven,
held her her her her him him home
houses i immeasurable immensity in in in in
inexpressibly is is is it it it its
journeying lands leave leave life like long looks
magical mean more night, no not not not
now now of of on one one one
passion puts quite red rises row same says
she she shush shyly sight sky so so
star star still stone such tell tells tells
that that the the the the the the
the there there thing through time to to
to to trees unravel untamed wanting watch what
when wind with with you you you you
your your

После применения unique() и последующего вызова erase() вектор texts выглядит следующим образом:

a alice alive almost alternately ancient
and as asks at beautiful becomes bird blows
blue bounded but by calling coat daddy dark
darkened darkening distant each either emma eternity falls
fear fiery flight flowing for grow hair has
he heaven, held her him home houses i
immeasurable immensity in inexpressibly is it its journeying
lands leave life like long looks magical mean
more night, no not now of on one
passion puts quite red rises row same says
she shush shyly sight sky so star still
stone such tell tells that the there thing
through time to trees unravel untamed wanting watch
what when wind with you your

Следующая наша задача – отсортировать строки по длине. Для этого мы воспользуемся не алгоритмом sort(), а алгоритмом stable_sort(), который сохраняет относительные положения равных элементов. В результате для элементов равной длины сохраняется алфавитный порядок. Для сортировки по длине мы применим собственную операцию сравнения "меньше". Один из возможных способов таков:

bool less_than( const string & s1, const string & s2 )
{
   return s1.size() < s1.size();
}

void process_vocab( vector<textwords, allocator> *pvec )
{
   // ...
   // отсортировать элементы вектора texts по длине,
   // сохранив также прежний порядок
   stable_sort( texts.begin(), texts.end(), less_than );

   // ...
}

Нужный результат при этом достигается, но эффективность существенно ниже, чем хотелось бы. less_than() реализована в виде одной инструкции. Обычно она вызывается как встроенная (inline) функция. Но, передавая указатель на нее, мы не даем компилятору сделать ее встроенной. Способ, позволяющий добиться этого–применение объекта-функции:

// объект-функция - операция реализована с помощью перегрузки
// оператора operator()
class LessThan {
public:
   bool operator()( const string & s1, const string & s2 )
                  { return s1.size() < s2.size(); }
};

Объект-функция – это класс, в котором перегружен оператор вызова operator(). В теле этого оператора и реализуется логика функции, в данном случае сравнение "меньше". Определение оператора вызова выглядит странно из-за двух пар скобок. Запись

operator()

говорит компилятору, что мы перегружаем оператор вызова. Вторая пара скобок

( const string & s1, const string & s2 )

задает передаваемые ему формальные параметры. Если сравнить это определение с предыдущим определением функции less_than(), мы увидим, что, за исключением замены less_than на operator(), они совпадают.
Объект-функция определяется так же, как обычный объект класса (правда, в данном случае нам не понадобился конструктор: нет членов, подлежащих инициализации):

LessThan lt;

Для вызова экземпляра перегруженного оператора мы применяем оператор вызова к нашему объекту класса, передавая необходимые аргументы. Например:

string st1( "shakespeare" );
string st2( "marlowe" );

// вызывается lt.operator()( st1, st2 );
bool is_shakespeare_less = lt( st1, st2 );

Ниже показана исправленная функция process_vocab(), в которой алгоритму

stable_sort() передается безымянный объект-функция LessThan():
void process_vocab( vector<textwords, allocator> *pvec )
{
   // ...
   stable_sort( texts.begin(), texts.end(), LessThan() );

   // ...
}

Внутри stable_sort() перегруженный оператор вызова подставляется в текст программы как встроенная функция. (В качестве третьего аргумента stable_sort() может принимать как указатель на функцию less_than(), так и объект класса LessThan, поскольку аргументом является параметр-тип шаблона. Подробнее об объектах-функциях мы расскажем в разделе 12.3.)
Вот результат применения stable_sort() к вектору texts:

a i
as at by he in is it no
of on so to and but for has
her him its not now one red row
she sky the you asks bird blue coat
dark each emma fear grow hair held home
life like long mean more puts same says
star such tell that time what when wind
with your alice alive blows daddy falls fiery
lands leave looks quite rises shush shyly sight
still stone tells there thing trees watch almost
either flight houses night, ancient becomes bounded calling
distant flowing heaven, magical passion through unravel untamed
wanting darkened eternity beautiful darkening immensity journeying alternately
immeasurable inexpressibly

Подсчитать число слов, длина которых больше шести символов, можно с помощью обобщенного алгоритма count_if() и еще одного объекта-функции – GreaterThan. Этот объект чуть сложнее, так как позволяет пользователю задать размер, с которым производится сравнение. Мы сохраняем размер в члене класса и инициализируем его с помощью конструктора (по умолчанию – значением 6):

#include <iostream>

class GreaterThan {
public:
   GreaterThan( int size = 6 ) : _size( size ){}
   int size() { return _size; }

   bool operator()( const string & s1 )
      { return s1.size() > 6; }

private:
   int _size;
};

Использовать его можно так:

void process_vocab( vector<textwords, allocator> *pvec )
{
   // ...
   // подсчитать число строк, длина которых больше 6

   int cnt = count_if( texts.begin(), texts.end(),
                       GreaterThan() );

   cout <<"Number of words greater than length six are "
        << cnt << endl;

   // ...
}

Этот фрагмент программы выводит такую строку:

Number of words greater than length six are 22

Алгоритм remove() ведет себя аналогично unique(): он тоже не изменяет размер контейнера, а просто разделяет элементы на те, что следует оставить (копируя их по очереди в начало контейнера), и те, что следует удалить (перемещая их в конец контейнера). Вот как можно воспользоваться им для исключения из коллекции слов, которые мы не хотим сохранять:

void process_vocab( vector<textwords, allocator> *pvec )
{
   // ...
   static string rw[] = { "and", "if", "or", "but", "the" };
   vector< string > remove_words( rw, rw+5 );

   vector< string >::iterator it2 = remove_words.begin();
   for ( ; it2 != remove_words.end(); ++it2 ) {
      // просто для демонстрации другой формы count()
     int cnt = count( texts.begin(), texts.end(), *it2 );
      cout << cnt  <<" instances removed:  "
           <<(*it2) <<endl;
    
      texts.erase(
           remove(texts.begin(),texts.end(),*it2 ),
           texts.end()
     );
   }

   // ...
}

Результат применения remove():

1 instances removed:  and
0 instances removed:  if
0 instances removed:  or
1 instances removed:  but
1 instances removed:  the

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

class PrintElem {
public:
   PrintElem( int lineLen = 8 )
      : _line_length( lineLen ), _cnt( 0 )
   {}

   void operator()( const string &elem )
   {
      ++_cnt;
      if ( _cnt % _line_length == 0 )
         { cout << '\n'; }

      cout <<elem << "";
   }

private:
   int _line_length;
   int _cnt;
};
void process_vocab( vector *pvec )
{
   // ...

   for_each( texts.begin(), texts.end(), PrintElem() );
}

Вот и все. Мы получили законченную программу, для чего пришлось лишь последовательно записать обращения к нескольким обобщенным алгоритмам. Для удобства мы приводим ниже полный листинг вместе с функцией main() для ее тестирования (здесь используются специальные типы итераторов, которые будут обсуждаться только в разделе 12.4). Мы привели текст реально исполнявшегося кода, который не полностью удовлетворяет стандарту C++. В частности, в нашем распоряжении были лишь устаревшие реализации алгоритмов count() и count_if(), которые не возвращают результат, а требуют передачи дополнительного аргумента для вычисленного значения. Кроме того, библиотека iostream отражает предшествующую принятию стандарта реализацию, в которой требуется заголовочный файл iostream.h.

#include <vector>
#include <string>
#include <algorithm>
#include <iterator>

// предшествующий принятию стандарта синтаксис <iostream>
#include <iostream.h>
class GreaterThan {
public:
   GreaterThan( int size = 6 ) : _size( sz ){}
   int size() { return _size; }

   bool operator()(const string &s1)
                  { return s1.size() >_size; }
private:
   int _size;
};

class PrintElem {
public:

   PrintElem( int lineLen = 8 )
      : _line_length( lineLen ), _cnt( 0 )
   {}

   void operator()( const string &elem )
   {
      ++_cnt;
      if ( _cnt % _line_length == 0 )
         { cout << '\n'; }

      cout << elem << " ";
   }

private:
   int _line_length;
   int _cnt;
};
class LessThan {
public:
   bool operator()( const string & s1,
                   const string & s2 )
   { return s1.size() < s2.size();    }
};

typedef vector textwords;
void process_vocab( vector<textwords, allocator> *pvec )
{
   if ( ! pvec ) {
     // вывести предупредительное сообщение
     return;
   }

   vector< string, allocator > texts;

   vector<textwords, allocator>::iterator iter;
   for ( iter = pvec-<>begin() ; iter != pvec->end(); ++iter )
      copy( (*iter).begin(), (*iter).end(),
              back_inserter( texts ));

   // отсортировать вектор texts
   sort( texts.begin(), texts.end() );

   // теперь посмотрим, что получилось
   for_each( texts.begin(), texts.end(), PrintElem() );

   cout << "\n\n";   // разделить части выведенного текста

   // удалить дубликаты
   vector<string, allocator>::iterator it;
   it = unique( texts.begin(), texts.end() );
   texts.erase( it, texts.end() );

   // посмотрим, что осталось
   for_each( texts.begin(), texts.end(), PrintElem() );
   cout << "\n\n";

   // отсортировать элементы
   // stable_sort сохраняет относительный порядок равных элементов
   stable_sort( texts.begin(), texts.end(), LessThan() );
   for_each( texts.begin(), texts.end(), PrintElem() );

   cout << "\n\n";

   // подсчитать число строк, длина которых больше 6
   int cnt = 0;

   // устаревшая форма count - в стандарте используется другая
   count_if( texts.begin(), texts.end(), GreaterThan(), cnt );

   cout <<"Number of words greater than length six are "
       << cnt  << endl;

   static string rw[] = { "and", "if", "or", "but", "the" };
   vector <string,allocator > remove_words( rw, rw+5 );

   vector <string, allocator >::iterator it2 = remove_words.begin();

   for ( ; it2 != remove_words.end(); ++it2 )
   {
      int cnt = 0;

      // устаревшая форма count - в стандарте используется другая
      count( texts.begin(), texts.end(), *it2, cnt );
    
      cout << cnt    << " instances removed:  "
           << (*it2)<< endl;

      texts.erase(
         remove(texts.begin(),texts.end(),*it2),
                texts.end()
      );
   }
   cout << "\n\n";
   for_each( texts.begin(), texts.end(), PrintElem() );
}

// difference_type - это тип, с помощью которого можно хранить результат
// вычитания двух итераторов одного и того же контейнера
// - в данном случае вектора строк ...
// обычно это предполагается по умолчанию

typedef vector<string,allocator>::difference_type diff_type;

// предшествующий принятию стандарта синтаксис для
#include <fstream.h>

main()
{
     vector<textwords, allocator> sample;
     vector<string,allocator>  t1, t2;
     string t1fn, t2fn;

    // запросить у пользователя имена входных файлов ...
    // в реальной программе надо бы выполнить проверку
     cout << "text file #1: " cin > >  t1fn;
     cout << "text file #2: " cin > >  t2fn;

    // открыть файлы
     ifstream infile1( t1fn.c_str());
     ifstream infile2( t2fn.c_str());

    // специальная форма итератора
    // обычно diff_type подразумевается по умолчанию ...
     istream_iterator<  string, diff_type >  input_set1( infile1 ), eos;
     istream_iterator<  string, diff_type >  input_set2( infile2 );

    // специальная форма итератора
     copy( input_set1, eos, back_inserter( t1 ));
     copy( input_set2, eos, back_inserter( t2 ));

     sample.push_back( t1 ); sample.push_back( t2 );
     process_vocab( &sample );
}

Упражнение 12.2
Длина слова – не единственная и, вероятно, не лучшая мера трудности текста. Другой возможный критерий – это длина предложения. Напишите программу, которая читает текст из файла либо со стандартного ввода, строит вектор строк для каждого предложения и передает его алгоритму count(). Выведите предложения в порядке сложности. Любопытный способ сделать это – сохранить каждое предложение как одну большую строку во втором векторе строк, а затем передать этот вектор алгоритму sort() вместе с объектом-функцией, который считает, что чем строка короче, тем она меньше. (Более подробно с описанием конкретного обобщенного алгоритма, а также с иллюстрацией его применения вы может ознакомиться в Приложении, где все алгоритмы перечислены в алфавитном порядке.)
Упражнение 12.3
Более надежную оценку уровня трудности текста дает анализ структурной сложности предложений. Пусть каждой запятой присваивается 1 балл, каждому двоеточию или точке с запятой – 2 балла, а каждому тире – 3 балла. Модифицируйте программу из упражнения 12.2 так, чтобы она подсчитывала сложность каждого предложения. Воспользуйтесь алгоритмом count_if() для нахождения каждого из знаков препинания в векторе предложений. Выведите предложения в порядке сложности.
12.3. Объекты-функции

Наша функция min() дает хороший пример как возможностей, так и ограничений механизма шаблонов:

template <typename Type>
const Type&
min( const Type *p, int size )
{
   Type minval = p[ 0 ];
   for ( int ix = 1; ix < size; ++ix )
      if ( p[ ix ] < minval )
         minval = p[ ix ];
      return minval;
}

Достоинство этого механизма – возможность определить единственный шаблон min(), который конкретизируется для бесконечного множества типов. Ограничение же заключается в том, что даже при такой конкретизации min() будет работать не со всеми.
Это ограничение вызвано использованием оператора "меньше": в некоторых случаях базовый тип его не поддерживает. Так, класс изображения Image может и не предоставлять реализации такого оператора, но мы об этом не знаем и пытаемся найти минимальный кадр анимации в данном массиве изображений. Однако попытка конкретизировать min() для такого массива приведет к ошибке компиляции:

error: invalid types applied to the < operator: Image < Image
(ошибка: оператор < применен к некорректным типам: Image < Image)

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

template < typename Type,
           bool (*Comp)(const Type&, const Type&)>
const Type&
min( const Type *p, int size, Comp comp )
{
   Type minval = p[ 0 ];
   for ( int ix = 1; ix < size; ++ix )
      if ( Comp( p[ ix ] < minval ))
         minval = p[ ix ];
      return minval;
}

Такое решение вместе с нашей первой реализацией на основе встроенного оператора "меньше" обеспечивает универсальную поддержку для любого типа, включая и класс Image, если только мы придумаем подходящую семантику для сравнения двух изображений. Основной недостаток указателя на функцию связан с низкой эффективностью, так как косвенный вызов не дает воспользоваться преимуществами встроенных функций.
Альтернативная стратегия параметризации заключается в применении объекта-функции вместо указателя (примеры мы видели в предыдущем разделе). Объект-функция – это класс, перегружающий оператор вызова (operator()). Такой оператор инкапсулирует семантику обычного вызова функции. Объект-функция, как правило, передается обобщенному алгоритму в качестве аргумента, хотя можно определять и независимые объекты-функции. Например, если бы был определен объект-функция AddImages, который принимает два изображения, объединяет их некоторым образом и возвращает новое изображение, то мы могли бы объявить его следующим образом:

AddImages AI;

Чтобы объект-функция удовлетворял нашим требованиям, мы применяем оператор вызова, предоставляя необходимые операнды в виде объектов класса Image:

Image im1("foreground.tiff"), im2("background.tiff");
// ...

// вызывает Image AddImages::operator()(const Image1&, const Image2&);
Image new_image = AI (im1, im2 );

У объекта-функции есть два преимущества по сравнению с указателем на функцию. Во-первых, если перегруженный оператор вызова – это встроенная функция, то компилятор может выполнить ее подстановку, обеспечивая значительный выигрыш в производительности. Во-вторых, объект-функция способен содержать произвольное количество дополнительных данных, например кэш или информацию, полезную для выполнения текущей операции.
Ниже приведена измененная реализация шаблона min() (отметим, что это объявление допускает также и передачу указателя на функцию, но без проверки прототипа):

template < typename Type,
           typename Comp >
const Type&
min( const Type *p, int size, Comp comp )
{
   Type minval = p[ 0 ];
   for ( int ix = 1; ix < size; ++ix )
      if ( Comp( p[ ix ] < minval ))
         minval = p[ ix ];
      return minval;
}

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

   1. из набора предопределенных арифметических, сравнительных и логических объектов-функций стандартной библиотеки;
   2. из набора предопределенных адаптеров функций, позволяющих специализировать или расширять предопределенные (или любые другие) объекты-функции;
   3. определенные нами собственные объекты-функции для передачи обобщенным алгоритмам. К ним можно применять и адаптеры функций.

В этом разделе мы рассмотрим все три источника объектов-функций.
12.3.1. Предопределенные объекты-функции

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

#include <functional>

Например, объект-функция, поддерживающий сложение, – это шаблон класса с именем plus. Для определения экземпляра, способного складывать два целых числа, нужно

#include <functional>

написать:

plus< int > intAdd;

Для выполнения операции сложения мы применяем перегруженный оператор вызова к intAdd точно так же, как и к классу AddImage в предыдущем разделе:

int ival1 = 10, ival2 = 20;
// эквивалентно int sum = ival1 + ival2;
int sum = intAdd( ival1, ival2 );

Реализация шаблона класса plus вызывает оператор сложения, ассоциированный с типом своего параметра – int. Этот и другие предопределенные объекты-функции применяются прежде всего в качестве аргументов обобщенных алгоритмов и обычно замещают подразумеваемую по умолчанию операцию. Например, по умолчанию алгоритм sort() располагает элементы контейнера в порядке возрастания с помощью оператора "меньше" базового типа. Для сортировки по убыванию мы передаем

vector< string > svec;
// ...

предопределенный шаблон класса greater, который вызывает оператор "больше":

sort( svec.begin(), svec.end(), greater<string>() );

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

class Int {
public:
   Int( int ival = 0 ) : _val( ival ) {}

   int operator-()          { return -_val;         }
   int operator%(int ival)  { return -_val % ival;  }

   bool operator<(int ival) { return -_val < ival;  }
   bool operator!()         { return -_val == 0;    }
private:
   int _val;
};

vector< string > svec;
string  sval1, sval2, sres;
complex cval1, cval2, cres;
int     ival1, ival2, ires;
Int     Ival1, Ival2, Ires;

15):

double  dval1, dval2, dres;

Кроме того, мы определяем два шаблона функций, которым передаем различные безымянные объекты-функции:

template <class FuncObject, class Type>
   Type UnaryFunc( FuncObject fob, const Type &val )
      { return fob( val ); }

template <class FuncObject, class Type>
   Type BinaryFunc( FuncObject fob,
                    const Type &val1, const Type &val2 )
      { return fob( val1, val2 ); }

12.3.2. Арифметические объекты-функции

Предопределенные арифметические объекты-функции поддерживают операции сложения, вычитания, умножения, деления, взятия остатка и вычисления противоположного по знаку значения. Вызываемый оператор – это экземпляр, ассоциированный с типом Type. Если тип является классом, предоставляющим перегруженную реализацию оператора, то именно эта реализация и вызывается.

plus<string> stringAdd;
// вызывается string::operator+()
sres = stringAdd( sval1, sval2 );

    * Сложение:

      plus<Type>

      dres = BinaryFunc( plus<double>(), dval1, dval2 );
      minus<int> intSub;
      ires = intSub( ival1, ival2 );

    * Вычитание:

      minus<Type>

      dres = BinaryFunc( minus<double>(), dval1, dval2 );
      multiplies<complex> complexMultiplies;
      cres = complexMultiplies( cval1, cval2 );

    * Умножение:

      multiplies<Type>

      dres = BinaryFunc( multiplies<double>(), dval1, dval2 );divides<int> intDivides;
      ires = intDivides( ival1, ival2 );

    * Деление:

      divides<Type>

    *

      dres = BinaryFunc( divides<double>(), dval1, dval2 );

    * Взятие остатка:

      modulus<Type>

      modulus<Int> IntModulus;
      Ires = IntModulus( Ival1, Ival2 );
      ires = BinaryFunc( modulus<int>(), ival1, ival2 );
      negate<int> intNegate;
      ires = intNegate( ires );

    * Вычисление противоположного значения:

       negate<Type>
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 628 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp