Шаблоны

Суть проблемы

Пусть у нас написан класс Array для типа int. Нам может понадобиться такой же класс для какого-нибудь другого типа, например double. Можно было бы скопировать код, заменить int на double и получить нужный класс. Но у такого решения есть недостатки: это долго, получается много одинакового кода, и, если в одном месте обнаружится ошибка, придётся менять код всех классов, а это может быть очень сложно, если программа большая.

Решение с помощью #define в стиле языка C

Простое решение с помощью #define

Определим тип Type с помощью операции #define.

class Array{
  Type * data;
  Type get(int i);
}

там где будем использовать Array

  #define Type int
  #include "array.h"
  #undef Type

У такого решения есть один большой недостаток: если мы объявим массив ещё и для типа double

  #define Type double
  #include "array.h"
  #undef Type

то эти классы (массивы для int и для double) будут называться одинаково, что может вызвать ошибки.Чтобы этого не происходило в объявлении можно написать

 class Array#Type 

и тогда они будут называться Arrayint , Arraydouble, и т.д.

Улучшенное решение

Макросы позволяют определить что-то вроде функций

#define DefineArray(name,Type)\
class name{\
   Type* data;\
  public:\
    Type get(int i){\
      return data[i];\
    }\
};

В конце каждой стороки, кроме последней, нужно ставить "\" чтобы препроцессор понял что это всё один макрос размером в несколько строк.
В основной программе пишем

  DefineArray(ArrayInt,int);

Далее всё происходит как при обычном и спользовании #define - код вставляется в программу и мы получаем класс ArrayInt - массив для целых чисел. Макрос с параметрами упрощает использование Array - для определения класса достаточно одной строчки и не нужно определять ещё один макрос - Type, из-за неправильного определения которого могут быть ошибки.


Минусы использования макросов

У такого решения есть минусы: во всех файлах исходного кода, где подключен .h-файл с подобными define-ами нельзя использовать слова DefineArray, Type и т. д. Компилироваться макрос будет только после подстановки, а не в месте определения, что может привести к очень странным сообщениям об ошибках, и отлаживать такой код очень сложно.

Решение с помощью шаблонов в стиле языка C++

Шаблоны - средство языка С++ , предназначенное для написания алгоритмов без привязки к типам данных.
Шаблон класса Array. Объявление:

template < typename T >
class Array{
    size_t mySize;
    T* myData;
  public:
    Array();
    Array(size_t size);
    Array(Array const & a);
    ~Array();

    Array& operator=(Array const & a);

    T & get(size_t i);
    void set(size_t i,T value);
    size_t size() const;
};

Т - переменная обозначающая тип данных.
Определение:

template < typename T >
  Array < T >::Array(){
    mySize = 0;
    myData = 0;
  }
template < typename T >
  Array < T >::Array(size_t size){
    mySize = size;
    myData = new T[size];
  }
template < typename T >
  Array < T >::Array(Array const &a){
    mySize = a.mySize;
    myData = new T[mySize];
    for (size_t i = 0; i != mySize; ++i){
      myData[i] = a.myData[i];
    }
  }
template < typename T >
  Array < T >::~Array(){
    delete[] myData;
  }
template < typename T >
  Array < T > & Array < T >::operator =(const Array < T > & a){
    delete[] myData;
    mySize = a.mySize;
    myData = new T[mySize];
    for (size_t i = 0; i != mySize; ++i){
      myData[i] = a.myData[i];
    }
    return *this;
  }
template < typename T >
  T& Array < T >::get(size_t i){
    return myData[i];
  }
template < typename T >
  void set(size_t i,T value){
    if (i >= 0) && (i < mySize) {
      data[i] = value;
    }
  }
template < typename T >
  size_t Array < T >::size() const{
    return mySize;
  }

Чтобы объявить переменную нужного нам типа в основной программе пишем

  Array < int > a(10);
  Array < double > b(10);

Так объявляются массив a переменых типа int размером 10, и соответственно массив b переменных типа double размером 10.
Таким образом мы получили универсальный класс - массив, который подходит почти для любых типов данных.

Замечания по поводу использования шаблонов

1) Тип, используемый в качестве аргумента шаблона, должен обеспечивать интерфейс, ожидаемый шаблоном. В примере с массивом необходим конструктор по умолчанию, так как он вызывается во время работы new[ ] в конструкторе Array.

2) Если Вы хотите внутри угловых скобок указать шаблонный класс, например

  Array < Array < int >     > 

то нужно ставить между закрывающими скобками пробел! Иначе компилятор воспримет две закрывающие скобки, как оператор сдвига.

3) Шаблоны можно вводить не только для классов, но и для функций

template < typename V >  
  void reverse(Array < V > & a){
    V t;
    for(size_t i = 0;i < a.size()/2;++i){
      t = a.get(i);
      a.set(i,a.get(a.size()-i-1));
      a.set(a.size()-i-1,t));
    }
  }

Вызов шаблонной функции:

  reverse < int >(a);

4) Можно создавать шаблоны, зависящие от несколькх параметров

  template < typename T,typename V > 
    void copy(Array < T > & a, Array < V > & b){
      if (a.size() != b.size()){
        return;
      }
      for (size_t i = 0;i != a.size(),++i){
        a.set(i,b.get(i))
      }

copy < int,double > (a,b);

5) Вместо typename можно писать class, это одно и тоже.

6)Компилятор может понять, какие аргументы у шаблона функции, если список параметров функции однозначно идентифицирует набор аргументов шаблона. Тогда можно написать так:

  copy(a,b);

Угадать не получится, например, тогда, когда аргумент шаблона определяет тип возвращаемого значения.

  template < typename T >
    T foo(...){
     .
     .
    }

7) Методы шаблонного класса могут в свою очередь быть шаблонными. Конструктор Array < T > от Array < V >

  Array(Array  < V > const & a){
    mySize = a.mySize;
    myData = new V [mySize];
    for(size_t i = 0;i < mySize;++i){
      data[i] = a.get(i);
    }
  }

Array < int > a(b);

8) При вызове конструктора, нам синтаксически некуда вписать его аргумент (см. пример выше), поэтому конструктор нужно писать так, чтобы компилятор мог угадать тип. То есть его аргумент должен определять аргумент шаблона. Тоже самое с оператором присваивания. Не бывает шаблонных деструкторов, конструкторов по умолчанию, потому что парметров нет - угадать нельзя, а синтаксически писать некуда.

9) как написать определение метода шаблонного класса?

template < typename T > 
  T& Array< T > ::get(size_t i){
    return myData[i];
  }

10) шаблонного метода шаблонного класса:

template < typename T > 
template < typename V > 
  Array< T > ::Array< V > (Array < V > const & a){
  .
  .
  }

конструктор копирования массива типа T из массива типа V

Особенности компиляции шаблонных методов и функций

Создание шаблонного класса, или функции (инстанцирование) происходит на этапе компиляции, поэтому необходимо чтобы весь код ,определяющий данный класс, был скомпилирован. Нельзя определение шаблонного класса поместить в отдельный файл срр, приходится всё писать в заголовочный файл.Но можно разбить его на 2: array.h и array_impl.h. Конфликтов это не вызовет, потому что все шаблонные функции по умолчанию inline.

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

  T t; 
  t.size(); 

то ошибки не будет если Т = int, но функция не используется.Это может вызвать некоторые проблемы: если мы пишем библиотеку, используя шаблоны, то нам сложно проверить корректность для всех типов, а пользователь когда-нибудь обнаружит ошибку компиляции. Для того чтобы избежать этого существует принудительное инстанцирование.

 template Array < int > 

или
 template void f < int > 

Это создаёт класс или функцию для данного аргумента - int , b и компилирует его.Так пользователь обнаружит ошибку сразу.

Обобщённое программирование и объектно-ориентированное программирование

Рассмотрим как можно решить одну и ту же задачу используя ООП или шаблоны(обобщённое программирование).Напишем функцию nsort,сортирующую каким-либо образом массив, применимую для любых типов.

Решение с помощью ООП

создадим класс comparable с определённой виртуальной функцией compare, и будем наследовать от него остальные, и определять для каждого свою функцию compare, возвращающую -1, 0 или 1, если первый аргумент меньше, равен или больше второго. Внутри функции nsort используем функцию compare.

  class comparable{
    virtual int compare(comparable* b)=0;
    .
    .
  }; 

  void nsort(comparable** m,size_t size){
  }

В массив comparable** заполняем указателями на наследников comparable (самими объектами нельзя, так как класс comparable виртуальный, нельзя создать объект такого типа и писть p = new comparable[10] ). Передаём массив функции nsort, и она внутри себя вызывает нужную функцию compare и сортирует массив.

Решение с помощью обобщённого программирования

Определим шаблонную функцию nsort, использующую внутри себя какой-либо оператор сравнения, например < . Будем передавать ей массив типа T. Единственное требование к T - наличие оператора < .

  template < typename T >
    void nsort(T* t,size_t size){
      .
      .
    }

Сравнение ООП и обобщённого программирования

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

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

Ещё один пример

Посмотим, как можно было бы написать переводчик с использованием шаблонов (см. лекцию про наследование и полиморфизм)

  template < typename Translator >
    void apply (Translator* t,string a){
      t->translate(a); 
    }

здесь не нужно наследовать все переводчики от одного

Использование typedef

Чтобы не писать такие длинные названия типов, как например

 List< Array < int >   >  

можно завести синонимы

 typedef List< Array < int >   > ListA 

если мы захотим использовать ListA в шаблонах, то нельзя забывать о пробелах между закрывающими скобками!

 B < ListA    > 

так же внутри шаблонного класса можно написать

template < typename T >
class Array
public:
  typedef T value; 
 

Это позволит использовать value внутри функций от данного шаблонного класса. Например, если мы хотим написать сортировку и для List и для Veсtor то внутри неё мы не знаем, какие типы в них хранятся и используем вместо них value