16.1.2. Шаблоны класса

Шаблон класса (class template) — своего рода проект для создания классов. Шаблоны классов отличаются от шаблонов функций, для которых компилятор не может вывести типы параметров шаблона. Вместо этого, как уже демонстрировалось не раз, для использования шаблона класса следует предоставить дополнительную информацию в угловых скобках после имени шаблона (см. раздел 3.3). Эта дополнительная информация — список аргументов шаблона, подставляемых вместо параметров шаблона.

Определение шаблона класса

В качестве примера реализуем шаблонную версию класса StrBlob (см. раздел 12.1.1). Присвоим шаблону имя Blob, указывающее, что он больше не специфичен только для строк. Как и класс StrBlob, этот шаблон будет предоставлять совместный (и проверяемый) доступ к своим членам. В отличие от класса, шаблон применяется к элементам практически любого типа. Подобно библиотечным контейнерам, используя шаблон Blob, пользователи должны будут определить тип элемента.

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

template <typename Т> class Blob {

public:

 typedef T value_type;

 typedef typename std::vector<T>::size_type size_type;

 // конструкторы

 Blob();

 Blob(std::initializer_list<T> il);

 // количество элементов в Blob

 size_type size() const { return data->size(); }

 bool empty() const { return data->empty(); }

 // добавление и удаление элементов

 void push_back(const T &t) {data->push_back(t);}

 // версия перемещения; см. p. 13.6.3

 void push_back(T &&t) { data->push_back(std::move(t)); }

 void pop_back();

 // доступ к элементу

 T& back();

 Т& operator[](size_type i); // определено в разделе 14.5

private:

 std::shared_ptr<std::vector<T>> data;

 // выдать сообщение, если data[i] недопустим

 void check(size_type i, const std::string &msg) const;

}

У шаблона Blob есть один параметр типа Т. Он используется везде, где ожидается тип элемента, хранимый классом Blob. Например, тип возвращаемого значения функции доступа к элементам Blob определен как Т&. Когда пользователь создаст экземпляр шаблона Blob, он использует параметр Т для замены конкретным типом аргумента шаблона.

За исключением списка параметров шаблона и использования Т вместо string, этот класс совпадает с тем, что было определено в разделе 12.1.1 и модифицировано в разделе 12.1.6, а также в главах 13 и 14.

Создание экземпляра шаблона класса

Как уже неоднократно упоминалось, при использовании шаблона класса следует предоставить дополнительную информацию. Как можно теперь утверждать, эта дополнительная информация является списком явных аргументов шаблона (explicit template argument), которые привязаны к параметрам шаблона. Компилятор использует эти аргументы для создания специфического экземпляра класса по шаблону.

Например, чтобы определить тип для шаблона Blob, следует предоставить тип элемента:

Blob<int> ia; // пустой Blob<int>

Blob<int> ia2 = {0,1,2,3,4}; // Blob<int> с пятью элементами

Оба объекта, ia и ia2, используют ту же специфическую для типа версию шаблона Blob (т.е. Blob<int>). Из этих определений компилятор создает экземпляр класса, который эквивалентен следующему:

template <> class Blob<int> {

 typedef typename std::vector<int>::size_type size_type;

 Blob();

 Blob(std::initializer_list<int> il);

 // ...

 int& operator[](size_type i);

private:

 std::shared_ptr<std::vector<int>> data;

 void check (size_type i, const std::string &msg) const;

};

Когда компилятор создает экземпляр класса из шаблона Blob, он переписывает его, заменяя каждый экземпляр параметра T заданным аргументом шаблона, которым в данном случае является int.

Компилятор создает разный класс для каждого заданного типа элемента:

// эти определения создают экземпляр двух разных типов Blob

Blob<string> names;  // Blob содержащий строки

Blob<double> prices; // другой тип элемента

Эти определения привели бы к созданию двух разных экземпляров класса: определение names создает класс Blob, в котором каждое вхождение Т заменено на string. Определение prices создает класс Blob, где Т заменено на double.

При каждом создании экземпляра шаблона класса получается независимый класс. У типа Blob<string> нет никаких отношений с другим типом класса Blob или специальных прав доступа к его членам.

Ссылки на тип шаблона в пределах шаблона

При чтении кода шаблона класса не следует забывать, что имя шаблона класса не является именем самого класса (см. раздел 3.3). Шаблон класса используется для создания экземпляра класса, при этом всегда используются аргументы шаблона.

Непонятным может показаться то, что код в шаблоне класса вообще не использует имя фактического типа (или значения) как аргумент шаблона. Вместо этого как аргументы шаблона зачастую используются собственные параметры. Например, переменная-член data использует два шаблона, vector и shared_ptr. Каждый раз, когда используется шаблон, следует предоставить аргументы шаблона. В данном случае предоставляемый аргумент шаблона имеет тот же тип, который используется при создании экземпляра шаблона Blob. Следовательно, определение переменной-члена data с использованием параметра типа шаблона Blob свидетельствует о том, что переменная-член data является экземпляром указателя shared_ptr на экземпляр шаблона vector, содержащего объекты типа Т.

std::shared_ptr<std::vector<T>> data;

При создании экземпляра специфического класса Blob, такого как Blob<string>, переменная-член data будет такой:

shared_ptr<vector<string>>

Если создать экземпляр Blob<int>, то переменная-член data будет такой: shared_ptr<vector<int>>, и т.д.

Функции-члены шаблонов класса

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

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

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

тип_возвращаемого_значения StrBlob::имя_члена(список_парам)

соответствующий член шаблона Blob будет выглядеть так:

template <typename Т>

тип_возвращаемого_значения Blob<Т>::имя_члена(список_парам)

Функция check() и функции доступа к членам

Начнем с определения функции-члена check(), проверяющей предоставленный индекс:

template <typename Т>

void Blob<T>::check(size_type i, const std::string &msg) const {

 if (i >= data->size())

  throw std::out_of_range(msg);

}

Кроме отличия в имени класса и использовании списка параметров шаблона, эта функция идентична первоначальной функции-члену класса StrBlob.

Оператор индексирования и функция back() используют параметр шаблона для определения типа возвращаемого значения, но в остальном они неизменны:

template <typename Т>

Т& Blob<T>::back() {

 check(0, "back on empty Blob");

 return data->back();

}

template <typename T>

T& Blob<T>::operator[](size_type i) {

 // если i слишком велико, check() передаст сообщение и предотвратит

 // доступ к несуществующему элементу

 check(i, "subscript out of range");

 return (*data)[i];

}

В первоначальном классе StrBlob эти операторы возвращали тип string&. Шаблонная версия возвращает ссылку на любой тип, использованный при создании экземпляра шаблона Blob.

Функция pop_back() почти идентична оригинальной функции-члену класса StrBlob:

template <typename Т> void Blob<T>::pop_back() {

 check(0, "pop_back on empty Blob");

 data->pop_back();

}

Оператор индексирования и функция-член back() перегружены как const. Оставим определение этих функций-членов и функции front() читателю в качестве самостоятельного упражнения.

Конструкторы Blob()

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

template <typename Т>

Blob<T>::Blob(): data(std::make_shared<std::vector<T>>()) { }

Здесь функция-член Blob() определяется в пределах шаблона Blob<T>. Как и стандартный конструктор StrBlob() (см. раздел 12.1.1), данный конструктор резервирует пустой вектор и сохраняет указатель на него в переменной data. Как уже упоминалось, в качестве аргумента резервируемого шаблона vector используется собственный параметр типа класса.

Точно так же конструктор, получающий параметр типа initializer_list, использует свой параметр типа T как тип элемента для своего параметра типа initializer_list:

template <typename Т>

Blob<T>::Blob(std::initializer_list<T> il):

 data(std::make_shared<std::vector<T>>(il)) { }

Подобно стандартному конструктору, этот конструктор резервирует новый вектор. В данном случае этот вектор инициализируется из параметра il.

Чтобы использовать этот конструктор, следует передать список инициализации, тип элементов которого совместим с типом элемента Blob:

Blob<string> articles = {"a", "an", "the"};

Параметр этого конструктора имеет тип initializer_list<string>. Каждый строковый литерал в списке неявно преобразуется в тип string.

Создание функций-членов шаблона класса

По умолчанию экземпляр функции-члена шаблона класса создается, только если программа использует эту функцию-член. Рассмотрим следующий код:

// создает экземпляр Blob<int> и конструктор initializer_list<int>

Blob<int> squares = {0,1,2,3,4,5,6,7,8,9};

// создает экземпляр Blob<int>::size() const

for (size_t i = 0; i != squares.size(); ++i)

 squares[i] = i*i; // создает экземпляр Blob<int>::operator[](size_t)

Этот код создает экземпляр класса Blob<int> и трех его функций-членов: operator[](), size() и конструктора initializer_list<int>().

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

По умолчанию экземпляр члена шаблона класса создается, только если он используется.

Упрощение использования имени шаблона класса в коде класса

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

// BlobPtr передает исключение при попытке доступа к несуществующему

// элементу

template <typename Т> class BlobPtr

public:

 BlobPtr(): curr(0) { }

 BlobPtr(Blob<T> &a, size_t sz = 0):

  wptr(a.data), curr(sz) { } T& operator*() const {

  auto p = check{curr, "dereference past end");

  return (*p)[curr]; // (*p) - вектор, на который указывает этот

                     // объект

 }

 // инкремент и декремент

 BlobPtr& operator++(); // префиксные операторы

 BlobPtr& operator--();

private:

 // если проверка успешна, check() возвращает shared_ptr на вектор

 std::shared_ptr<std::vector<T>>

  check(std::size_t, const std::string&) const;

 // хранит weak_ptr, а значит, базовый вектор может быть удален

 std::weak_ptr<std::vector<T>> wptr;

 std::size_t curr; // текущая позиция в пределах массива

};

Внимательные читатели, вероятно, обратили внимание на то, что префиксные функции-члены инкремента и декремента шаблона класса BlobPtr возвращают тип BlobPtr&, а не BlobPtr<T>&. В области видимости шаблона класса компилятор рассматривает ссылки на сам шаблон так, как будто были подставлены аргументы шаблона, соответствующие собственным параметрам. Таким образом, этот код эквивалентен следующему:

BlobPtr<T>& operator++();

BlobPtr<T>& operator--();

Использование имени шаблона класса вне тела шаблона

При определении функций-членов вне тела шаблона класса следует помнить, что код находится не в области видимости класса, пока не встретилось имя класса (см. раздел 7.4):

// постфикс: осуществляет инкремент/декремент объекта, но возвращает

// неизменное значение

template <typename Т>

BlobPtr<T> BlobPtr<T>::operator++(int) {

 // никакой проверки здесь не нужно; ее выполнит вызов префиксного

 // инкремента

 BlobPtr ret = *this; // сохранить текущее значение

 ++*this;             // перемещение на один элемент; префиксный ++

                      // проверяет инкремент

 return ret;          // возвратить сохраненное состояние

}

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

BlobPtr<T> ret = *this;

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

Шаблоны классов и дружественные отношения

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

Дружественные отношения "один к одному"

Наиболее распространенная форма дружественных отношений между шаблоном класса и другим шаблоном (класса или функции) подразумевает дружбу между соответствующими экземплярами класса и его друга. Например, класс Blob должен объявить дружественным класс BlobPtr и шаблонную версию оператора равенства класса Blob (первоначально определенную для класса StrBlob в упражнении раздела 14.3.1).

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

// для объявления дружественных отношений в шаблоне Blob нужны

// предварительные объявления

template <typename> class BlobPtr;

template <typename> class Blob; // необходимо для параметров operator==

template <typename T>

 bool operator==(const Blob<T>&, const Blob<T>&);

template <typename T> class Blob {

 // каждый экземпляр Blob предоставляет доступ к версии BlobPtr и

 // оператору равенства экземпляра, созданного с тем же типом

 friend class BlobPtr<T>;

 friend bool operator==<T>

  (const Blob<T>&, const Blob<T>&);

 // другие члены, как в разделе 12.1.1

};

Начнем с объявления Blob, BlobPtr и operator== шаблонами. Эти объявления необходимы для объявления параметра в функции operator== и дружественных объявлений в шаблоне Blob.

Объявления дружественными используют параметр шаблона Blob как собственный аргумент шаблона. Таким образом, дружба ограничивается этими экземплярами шаблона BlobPtr и оператора равенства, которые создаются с тем же типом:

Blob<char> ca; // BlobPtr<char> и operator==<char> друзья

Blob<int> ia;  // BlobPtr<int> и operator==<int> друзья

Члены класса BlobPtr<char> могут обращаться к не открытым членам объекта ca (или любого другого объекта класса Blob<char>), но объект ca не имеет никаких специальных прав доступа к объекту ia (или любому другому объекту класса Blob<int>) или любому другому экземпляру класса Blob.

Общие и специфические дружественные отношения шаблонов

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

// предварительное объявление необходимо для объявления дружественных

// отношений со специфическим экземпляром шаблона

template <typename Т> class Pal;

class С { // С - обычный, не шаблонный класс

 friend class Pal<C>; // экземпляр Pal создается с классом С как

                      // дружественным

 // все экземпляры Раl2 дружественны С;

 // при предоставлении дружественных отношений всем экземплярам

 // предварительное объявление не обязательно

 template <typename Т> friend class Раl2;

};

template <typename T> class C2 { // C2 - сам шаблон класса

 // у каждого экземпляра C2 есть тот же экземпляр Pal, что и у друга

 friend class Pal<T>; // объявление шаблона для Pal должно быть в

                      // области видимости

 // все экземпляры Раl2 - друзья каждого экземпляра C2; необходимо

 // предварительное объявление

 template <typename X> friend class Раl2;

 // Pal3 - не шаблонный класс, являющийся другом каждого экземпляра C2

 friend class Раl3; // предварительное объявление для Раl3

                    // не обязательно

};

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

Объявление параметра типа шаблона дружественным

По новому стандарту параметр типа шаблона можно сделать дружественным:

template <typename Type> class Bar {

 friend Type; // предоставить доступ к типу, используемому для создания

              // экземпляра Bar

 // ...

};

Здесь указано, что, независимо от используемого для создания экземпляра типа, класс Bar будет дружественным. Таким образом, для некоего типа под названием Foo он был бы другом для Bar<Foo>, а тип Sales_data — другом для Bar<Sales_data> и т.д.

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

Псевдонимы типа шаблона

Экземпляр шаблона класса определяет тип класса, и, подобно любому другому типу класса, для экземпляра класса можно определить псевдоним при помощи ключевого слова typedef (см. раздел 2.5.1):

typedef Blob<string> StrBlob;

Это определение типа позволит выполнить код, написанный в разделе 12.1.1, используя текущую версию шаблона Blob, экземпляр которого создан для типа string. Поскольку шаблон не тип, ключевое слово typedef к шаблону неприменимо. Таким образом, нет никакого способа определить typedef для шаблона Blob<Т>.

Однако новый стандарт позволяет определять псевдоним типа для шаблона класса:

template<typename Т> using twin = pair<T, Т>;

twin<string> authors; // authors - это pair<string, string>

где имя twin определено как синоним для пар с одинаковыми типами членов. Пользователям типа twin достаточно определить его только однажды.

Псевдоним типа шаблона — это синоним для целого семейства классов:

twin<int> win_loss; // win_loss - это pair<int, int>

twin<double> area;  // area - это pair<double, double>

Как и при использовании шаблона класса, при использовании псевдонима twin следует указать, какой именно вид twin необходим.

При определении псевдонима типа шаблона можно зафиксировать один или несколько параметров шаблона:

template <typename Т> using partNo = pair<T, unsigned>;

partNo<string> books; // books - это pair<string, unsigned>

partNo<Vehicle> cars; // cars - это pair<Vehicle, unsigned>

partNo<Student> kids; // kids - это pair<Student, unsigned>

Здесь имя partNo определено как синоним семейства типов, которые являются парами, вторая переменная-член которого имеет тип unsigned. Пользователи partNo определяют тип первой переменной-члена пары, но не второй.

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

Подобно любому другому классу, шаблон класса способен объявить статические члены (см. раздел 7.6):

template <typename T> class Foo {

public:

 static std::size_t count() { return ctr; }

 // другие члены интерфейса

private:

 static std::size_t ctr;

 // другие члены реализации

};

где Foo — шаблон класса, у которого есть открытая статическая функция-член count() и закрытая статическая переменная-член ctr. У каждого экземпляра шаблона Foo будет собственный экземпляр статических членов. Таким образом, для любого конкретного типа X будет по одной переменной Foo<X>::ctr и одной функции Foo<X>::count(). Все объекты типа Foo<X> будут совместно использовать ту же переименую ctr и функцию count(). Например:

// создает экземпляр статических членов Foo<string>::ctr

// и Foo<string>::count

Foo<string> fs;

// все три объекта совместно используют те же члены Foo<int>::ctr

// и Foo<int>::count

Foo<int> fi, fi2, fi3;

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

template <typename Т>

size_t Foo<T>::ctr = 0; // определение и инициализация ctr

Подобно любым другим членам шаблона класса, начнем с определения списка параметров шаблона, сопровождаемого типом и именем определяемого члена. Как обычно, имя члена включает имя класса, которое включает для класса, созданного из шаблона, его аргументы шаблона. Таким образом, когда класс Foo создается как экземпляр для специфического типа аргумента шаблона, для этого класса будет создан отдельный экземпляр переменной ctr и инициализирован значением 0.

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

Foo<int> fi;                 // создает экземпляр класса Foo<int>

                             // и статической переменной-члена ctr

auto ct = Foo<int>::count(); // создает экземпляр Foo<int>::count()

ct = fi.count();             // использует Foo<int>::count()

ct = Foo::count();           // ошибка: экземпляр какого именно

                             // шаблона создается?

Как и любая другая функция-член, экземпляр статической функции-члена создается только при его использовании в программе.

Упражнения раздела 16.1.2

Упражнение 16.9. Что такое шаблон функции? Что такое шаблон класса?

Упражнение 16.10. Что происходит при создании экземпляра шаблона класса?

Упражнение 16.11. Следующее определение шаблона List неправильно. Как его исправить?

template <typename elemType> class ListItem;

template <typename elemType> class List {

public:

 List<elemType>();

 List<elemType>(const List<elemType> &);

 List<elemType>& operator=(const List<elemType> &);

 ~List();

 void insert(ListItem *ptr, elemType value);

private:

 ListItem *front, *end;

};

Упражнение 16.12. Напишите собственные версии шаблонов Blob и BlobPtr, включая все константные члены, которые не были представлены в тексте.

Упражнение 16.13. Объясните, какой вид дружественных отношений вы выбрали бы для операторов равенства и сравнения шаблона BlobPtr.

Упражнение 16.14. Напишите шаблон класса Screen, который использует параметры значения для определения высоты и ширины экрана.

Упражнение 16.15. Реализуйте операторы ввода и вывода для своего шаблона Screen. Какие друзья необходимы классу Screen (если таковые вообще имеются) для работы операторов ввода и вывода? Объясните, зачем нужно каждое объявление дружественным (если таковые вообще имеются).

Упражнение 16.16. Перепишите класс StrVec (см. раздел 13.5), как шаблон Vec.

Более 800 000 книг и аудиокниг! 📚

Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением

ПОЛУЧИТЬ ПОДАРОК