Перейти к содержанию

Си++/Обобщённое программирование

Материал из Викиучебника — открытых книг для открытого мира

Обобщённое программирование (Generic programming)

[править]

Введение, или альтернативные подходы

[править]

Предположим, мы написали суперконтейнер (то есть тип, предназначенный для хранения) чисел типа int:

class IntContainer {
public:
  void add (int value);
  int index_of (int value);
  int get_count ();
  int get_value (int index);
private: 
    
};

Однако нам грустно тратить силы, создавая суперконтейнеры для каждого типа данных, который мы собираемся использовать. Тем более, что их код будет похож, как две капли воды. Что же делать?

Есть старый приём, использовавшийся в Си. Он оригинален и неожидан, как и всё в Си. Применительно к нашей ситуации, он выглядит так (разумеется, в Си классов не было, и поэтому там всё выглядело иначе):

#define DEFINE_CONTAINER(T,N) \
  class N ## Container { \
  public: \
    void add (T value); \
    int index_of (T value); \
    int get_count (); \
    T get_value (int index); \
  private: \ 
       \
  };

Это — длинное определение макроса «DEFINE_CONTAINER». Оно занимает несколько строк благодаря использованию «символа продолжения макроса на следующую строку» — «\». Символы «##» используются, чтобы соединить вместе аргумент «N» и слово «Container», получив одно слово — название определяемого класса.

Используется это так:

DEFINE_CONTAINER (int, Int)
IntContainer apples;
int main () {
  apples.add (11);
  apples.add (13);
}

Строка «DEFINE_CONTAINER (int, Int)» раскрывается в определение класса «IntContainer», который мы и используем далее. Если нам понадобится контейнер для другого типа, мы можем написать «DEFINE_CONTAINER (double, Double)», «DEFINE_CONTAINER (const char *, ConstString)». Получатся классы «DoubleContainer» и «ConstStringContainer».

Можно обойтись и без макросов. Мы могли бы создать файлы «FooContainer._h_» и «FooContainer._cpp_». В них можно было определить контейнер «FooContainer» для типа «foo». Далее, вы используете ваш любимый текстовый редактор, чтобы скопировать их, например, в «IntContainer.h» и «IntContainer.cpp», заменив все вхождения слов «Foo» и «foo» на «Int» и «int» соответственно. Этот процесс можно автоматизировать. В нужный момент вы просто включите файл «IntContainer.h» и будете им пользоваться, как будто он написан вручную.

То, чем вы занимаетесь при любом из этих подходов, можно назвать словом «generic programming», или просто программирование шаблонов. (Вполне понятно, почему «DEFINE_CONTAINER» и «FooContainer._h_/._cpp_» можно назвать шаблонами контейнеров.)

Однако, в Си++ есть родной (встроенный) способ создавать шаблоны, без некрасивых макросов и неудобных манипуляций с файлами.

Шаблоны классов

[править]

Шаблоны в Си++ создаются с помощью ключевого слова «template». Предыдущий пример можно переписать в виде:

template <typename T>
class Container {
public:
  void add (T value);
  int index_of (T value);
  int get_count ();
  T get_value (int index);
private: 
   
};

Мы имеем шаблон, называемый «Container». Этим шаблоном, как и любым другим, в чистом виде пользоваться нельзя; пользоваться можно только его экземплярами для конкретных типов.

В Си++ экземпляр шаблона «Container» для типа «int» будет называться «Container<int>»; угловые скобки и всё между ними следует рассматривать как часть названия класса. (Хотя, безусловно, вы можете вставлять пробелы по вкусу, например «Container < int >».)

Си++ сам создаёт нужные экземпляры шаблона, то есть никаких «DEFINE_CONTAINER» и тому подобных вещей вы не пишете вообще. Вы просто пользуетесь нужными экземплярами шаблонов. Например, мы можем написать:

Container <int> apples;
int main () {
  apples.add (7);
  apples.add (11);
}

Что ещё нужно вам узнать прямо сейчас?

  • В чистом виде название «Container» не используется почти ни в каких случаях. (Исключения упомянем позже.) Всегда используется или «template <typename T> class Container», если мы говорим о шаблоне в общем, или же «Container <int>», если мы говорим о конкретном экземпляре шаблона.
  • Безусловно, у шаблонов может быть несколько параметров.
  • Параметрами шаблонов могут быть не только типы. Например, вы можете описать шаблон «template <typename T, unsigned size> class Array», и воспользоваться его экземпляром «Array <float, 20>».

Шаблоны функций

[править]

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

Вот как она выглядит на Си++:

template <typename T>
void swap (T &left, T &right) {
  T temp = left;
  left = right;
  right = temp;
}

Аналогично шаблонам классов, это — шаблон функции. (Часто говорят — «шаблонный класс», «шаблонная функция».) Экземпляр этой функции, обменивающий местами два значения типа «int», называется «swap <int>».

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

int main () {
  int a = 3, b = 5;
  swap (a, b);
  /* теперь a = 5, b = 3 */
}

Поскольку оба аргумента функции имеют тип «int», компилятор знает, что нужно вызвать «swap <int> (a, b)». Разумеется, всегда можно указать экземпляр функции вручную, самостоятельно вызвав «swap <int> (a, b)».

Во всём остальном шаблоны функций не отличаются от шаблонов классов. Поэтому дальнейшее обсуждение относится к ним обоим.

Параметры шаблонов

[править]

Вы уже знаете почти всё о параметрах. Осталось сказать только самую малость.

Параметры, как вы уже знаете, бывают двух видов:

  • типы;
  • числа, строки и другие простые типы.

Теперь о них поподробнее.

«typename» и «class»

[править]

Чаще всего применяются, конечно, параметры-типы. Есть два совершенно равноценных способа сделать такой параметр:

/* первый способ */
template <typename T> class Foo {  };
/* второй способ */
template <class T> class Foo {  };

Об этом не стоило бы и говорить. Слова «typename» и «class» в этом месте — полные синонимы. Видимо, предпочтительнее использовать «typename». На самом деле, в Си++ слова «тип» и «класс» — почти всегда синонимы.

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

template <typename T>
class Foo {
  T baz;
  void bar () {
    baz.someCoolMethod (15);
  }
};

то явно предполагается, что у переданного типа есть метод «someCoolMethod», принимающий один аргумент типа «int». Но компилятор об этом не знает, поэтому в качестве параметра вы всё равно можете передать любой тип. Однако, когда компилятор попытается создать экземпляр шаблона, он будет ругаться: «У типа T не определён метод someCoolMethod!» (и дальше ещё длинная информация о том, что он создаёт экземпляр шаблона, определённого там-то, с такими-то параметрами).

Применений у параметров шаблона, являющихся типами, — тьма. Вот самое обычное применение:

template <typename T>
class Foo {
  T baz;
    
}

Вот необычное применение:

template <typename T>
class Foo : public T {
    
}

Простые типы как параметры шаблона

[править]

Параметры в виде простых типов используются сравнительно реже. Выглядят они так:

template <unsigned BufSize>
class Foo {
  char buffer [BufSize];
};

TODO: какие именно типы можно использовать в качестве параметров? Интегральные, указатели/ссылки на экспортированные статические объекты (например, на функции).

TODO: найти ещё примеры применения этого вида параметров. static assert.

Значения по-умолчанию у параметров шаблона

[править]

Шаблоны могут иметь значения по умолчанию для некоторых параметров. Это очень похоже на аналогичную возможность для функций. Пример:

template <typename ValueType, unsigned, unsigned MaxSize = 100>
class Foo {
public:
  int add (ValueType value);
  int index_of (ValueType value);
  int get_count ();
  ValueType get_item (int index);
protected:
  ValueType data [MaxSize];
  unsigned count;
};

Здесь мы имеем контейнер, в котором может храниться до «MaxSize» объектов типа «ValueType». Если мы не укажем параметр «MaxSize» при создании экземпляра, то будет использовано значение по умолчанию — 100. (Заметьте, что это ужасно плохой стиль программирования. Пользователь вашего класса может встроить в свою программу ограничение в 100 объектов, даже не подозревая об этом. Никогда так не делайте.)

Параметры-типы тоже могут иметь значения по умолчанию. Например, в STL повсеместно применяются объекты-аллокаторы. Однако вы чаще всего о них не задумываетесь — именно благодаря значениям по умолчанию. Например, класс «list» из STL может иметь такой заголовок:

template <typename ValueType, typename AllocatorType = allocator <ValueType> >
class list {
    
};

Значением по умолчанию для параметра «AllocatorType» является тип «allocator <ValueType>». Заметьте, как мы используем значение одного параметра («ValueType»), чтобы задать значение по умолчанию для другого параметра. Это возможно, потому что второй параметр описан после первого. Вы везде пишете:

int main () {
  list <int> oranges;
  oranges.push_back (11);
  oranges.push_back (13);
}

и можете вообще не знать, что такое аллокатор. Но самые продвинутые могут написать свой аллокатор, и использовать экземпляр «list» по имени «list <int, MyAllocator>».

Синтаксические особенности шаблонов

[править]

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

Определение функций-членов (методов)

[править]

Как и всегда, определить небольшой метод можно и внутри класса:

template <typename T>
class Foo {
  void bar () { 
      
  }
};

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

template <typename T>
void Foo <T>::bar () { 
    
}

Он может показаться ужасным, но иного выхода нет. Кроме того, если вы класс определяете в заголовочном файле, то и все определения его методов вы должны поместить в заголовочный файл, а не в соответствующий «.cpp». (Как правило, у шаблонных классов вообще нет «.cpp»-файла, весь код помещается в «.h»-файл.) ЗЫ. Но иногда исхитряются следующим образом: описывают интерфейс шаблона в «.h»-файле, но в его конце добавляют строчку #include template_file.cpp, где файл template_file.cpp содержит реализацию шаблона.

Конструктор и деструктор

[править]

Конструктор и деструктор определяются также, как и обычные методы:

template <typename T>
Foo <T>::Foo () { 
    
}

template <typename T>
Foo <T>::~Foo () { 
    
}

Заметьте, имена конструкторов и деструктора — немногие случаи, когда имя класса употребляется «в чистом виде».

Определение статических данных-членов

[править]

Если вы знаете, о чём идёт речь, то следующий пример скажет вам всё необходимое:

template <typename T>
class Foo {
public:
    static int x;
};

template <typename T>
int Foo <T>::x;

Экземпляр шаблона как параметр другого шаблона

[править]

Мы уже видели пример того, как это делается, выше, в разделе «Значения по-умолчанию для параметров». Немного другой пример:

template <typename T>
class Foo {  };

template <typename T>
class Bar {  };

int main () {
  Bar <Foo <int> > bar;
}

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

Шаблон как параметр другого шаблона

[править]

В качестве параметра шаблона можно также передавать другой шаблон:

template< template< class T > class U >
class Foo {
 public:
   U< int > member;
};

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

#include <vector>  
/*
    Шаблон из стандартной библиотеки C++
  template<class Type, class Allocator = std::allocator<Type> 
  class vector
  {
    ... ...
  }
*/

#include <list>
/*
    Шаблон из стандартной библиотеки C++
  template<class Type, class Allocator = std::allocator<Type> 
  class list
  {
    ... ...
  }
 */

template< 
    template<class Type, class Allocator = std::allocator<Type> > class Container, 
    class T 
>

class Bar {
    ... ... ... 
    Container<T> collection;
    ... ... ... 
};

class Qux
{
   ... ... ... 
};

Bar< std::vector, int >  bar1;  // bar1.collection имеет тип std::vector<int>
Bar< std::list,   Qux >  bar2;  // bar2.collection имеет тип std::list<Qux>

Теперь bar1 использует для хранения int std::vector, а bar2 - std::list для хранения Qux (std::vector и std::list — шаблоны из стандартной библиотеки C++. Обратите внимание, что они имеют два шаблонных параметра, один из которых задан в стандартной библиотеке по умолчанию, и мы при написании шаблонного параметра шаблона должны указать их оба).

Ключевое слово «typename»

[править]

Рассмотрим класс:

struct Foo {
  typedef int my_type;
};

Вся суть этого класса в том, что «Foo::my_type» обозначает некий тип. Теперь напишем шаблонный класс, использующий этот факт. (Внимание, пример неправильный!)

template <typename T>
class Bar {
  T::my_type x;
};

Мы пытаемся воспользоваться типом «my_type», определённым в «T». Однако в силу некоторых причин компилятор сам не хочет понимать, что «T::my_type» в данном случае обозначает тип. Мы должны ему в этом помочь, поставив перед всеми такими типами ключевое слово «typename»:

template <typename T>
class Bar {
  typename T::my_type x;
};

В этом фрагменте кода утверждается, что «T::my_type» — это именно тип. На современных компиляторах всегда, ссылаясь на тип, определённый внутри класса, передаваемого параметром шаблона, необходимо использовать «typename».

Заметьте, все определения методов шаблонного класса должны быть видны изо всех мест, где он используется. Это значит, что если вы, например, попытаетесь использовать механизм раздельной компиляции, включая в заголовочный файл(*.h) только обявление шаблона, то компилятор просто не сможет его инстанцировать, т.ё. вывести из шаблона конретный тип. Поэтому приходится включать в заголовочные файл(или в единицу трансляции где шаблон объявлен и используется) помимо объявления ещё и определение шаблона, что приводит к разбуханию кода. Существует решение этой проблемы в виде механизма экспорта шаблона, предложенного компанией Silicon Graphics и в своём виде поддерживаемого некоторыми компиляторами различных разработчиков. Однако это решение потребовало изменение стандарта Си++ 1996г. и нашло реализацию далеко не во всех компиляторах, например в компиляторе от Microsoft включённого в продукт Microsoft Visual Studio.

Особенности компиляции шаблонов

[править]

Помещаем шаблоны в файлы

[править]

Проверка ошибок при работе с шаблонами

[править]

Специализация

[править]

Наследование и шаблоны

[править]