Правило 42: Усвойте оба значения ключевого слова typename
Правило 42: Усвойте оба значения ключевого слова typename
Вопрос: какая разница между «class» и «typename» в следующем объявлении шаблона:
template <class T> class Widget; // использует “class”
template <typename T> class Widget; // использует “typename”
Ответ: никакой. Когда в шаблоне объявляется параметр типа, class и type-name означают абсолютно одно и то же. Некоторые программисты предпочитают всегда писать class, потому что это слово короче. Другие (включая меня) предпочитают typename, поскольку оно говорит о том, что параметром не обязательно должен быть тип класса. Некоторые разработчики используют typename, когда допускается любой тип, и резервируют слово class для случаев, когда допускается только тип, определяемый пользователем. Но с точки зрения C++, class и typename в объявлении параметра шаблона означают в точности одно и то же.
Однако не всегда в C++ ключевые слова class и typename эквивалентны. Иногда вы обязаны использовать typename. Чтобы понять – когда именно, поговорим о двух типах имен, на которые можно ссылаться в шаблоне.
Предположим, что у нас есть шаблон функции, принимающей в качестве параметра совместимый с STL-контейнер, содержащий объекты, которые могут быть присвоены величинам типа int. Далее предположим, что эта функция просто печатает значение второго элемента. Это не очень содержательная функция, которая к тому же и реализована по-дурацки. Как я уже говорил, она даже не будет компилироваться, но забудьте об этом на время – все это не так глупо, как кажется:
template <typename C> // печатает второй
void print2nd(const C& container) // элемент контейнера
{ // это некорректный C++!
if (container.size() >= 2) {
C::const_iterator iter(container.begin()); // получить итератор,
// указывающий на первый
// элемент
++iter; // сместиться на второй
// элемент
int value = *iter; // скопировать элемент в int
std::cout << value; // напечатать int
}
}
Я выделил в этой функции две локальные переменные – iter и value. Типом iter является C::const_iterator – он зависит от параметра шаблона C. Имена в шаблоне, которые зависят от параметра шаблона, называются зависимыми именами. Зависимое имя внутри класса я буду называть вложенным зависимым именем. C::const_iterator – это вложенное зависимое имя. Фактически это даже вложенное зависимое имя типа, то есть вложенное имя, которое относится к типу.
Другая локальная переменная в print2nd – value – имеет тип int, а int – это имя, которое не зависит ни от какого параметра шаблона. Такие имена называются независимыми.
Вложенные зависимые имена могут стать причиной затруднений на этапе синтаксического анализа исходного текста компилятором. Например, предположим, что мы реализуем print2nd еще более глупо, написав в начале такой код:
template <typename C> // печатает второй элемент контейнера
void print2nd(const C& container) // это некорректный C++!
{
C::const_iterator *x;
...
}
Выглядит так, будто мы объявили x как локальную переменную – указатель на C::const_iterator. Но это только видимость, поскольку мы «знаем», что C::const_iterator является типом. А что, если в классе C есть статический член данных по имени const_iterator и что, если x будет именем глобальной переменной? В этом случае приведенный код не будет объявлять локальную переменную, а окажется умножением C::const_iterator на x! Звучит невероятно, но это возможно, и авторы синтаксических анализаторов исходного кода на C++ должны позаботиться обо всех возможных вариантах входных данных, даже самых сумасшедших.
Пока о C ничего не известно, мы не можем узнать, является ли C::const_iterator типом или нет, а во время разбора шаблона print2nd компилятор ничего о C не знает. В C++ предусмотрено правило, разрешающее эту неопределенность: если синтаксический анализатор встречает вложенное зависимое имя в шаблоне, он предполагает, что это не имя типа, если только вы не укажете это явно. По умолчанию вложенные зависимые имена не являются типами. Есть исключение из этого правила, о котором я расскажу чуть ниже.
Имея это в виду, посмотрите опять на начало print2nd:
template <typename C>
void print2nd(const C& container)
{
if (container.size() >= 2) {
C::const_iterator iter(container.begin()); // предполагается, что
... // это не имя типа
Теперь должно быть ясно, почему это некорректный C++. Объявление iter имеет смысл только в случае, если C::const_iterator является типом, но мы не сообщили C++ об этом, потому C++ предполагает, что это не так. Чтобы исправить ситуацию, мы должны сообщить C++, что C::const_iterator – это тип. Для этого мы помещаем ключевое слово typename непосредственно перед ним:
template <typename C> // это корректный С++
void print2nd(const C& container)
{
if (container.size() >= 2) {
typename C::const_iterator iter(container.begin());
...
}
}
Общее правило просто: всякий раз, когда вы обращаетесь к вложенному зависимому имени в шаблоне, вы должны предварить его словом typename (скоро я опишу исключение).
Слово typename следует использовать для идентификации только вложенных зависимых имен типов; для других имен оно не применяется. Вот пример шаблона функции, который принимает и контейнер, и итератор для этого контейнера:
template <typename C> // допускается typename (как и “class”)
void f(const C& container, // typename не допускается
typename C::iterator iter); // typename требуется
C не является вложенным зависимым именем типа (оно не вложено внутрь чего-либо, зависимого от параметра шаблона), поэтому его не нужно предварять словом typename при объявлении контейнера, но C::iterator – это вложенное зависимое имя типа, поэтому перед ним следует поставить typename.
Из правила «typename должно предварять вложенные зависимые имена типов» есть исключение: typename не должно предварять вложенные зависимые имена типов в списке базовых классов или в идентификаторе базового класса в списке инициализации членов. Например:
template <typename T>
class Derived: public Base<T>::Nested { // список базовых классов:
public: // typename не допускается
explicit Derived(int x)
:Base<T>::Nested(x) // идентификатор базового класса
{ // в списке инициализации членов:
// typename не допускается
typename Base<T>::Nested temp; // использование вложенного
... // зависимого имени типа не как
} // идентификатора базового
... // класса в списке инициализации
}; // членов: typename необходимо
Такая несогласованность несколько раздражает, но по мере приобретения опыта вы перестанете ее замечать.
Рассмотрим еще один пример использования typename, потому нечто подобное можно встретить в реальном коде. Предположим, что мы пишем шаблон функции, которая принимает итератор, и хотим сделать локальную копию – temp – объекта, на который этот итератор указывает. Это можно сделать примерно так:
template <typename IterT>
void workWithIterator(IterT iter)
{
typename std::iterator_traits<IterT>::value_type temp(*iter);
...
}
Не пугайтесь при виде выражения std::iterator_traits<IterT>::value_type. Здесь просто используются стандартные классы-характеристики (traits) (см. правило 47). Так, на C++ говорят «тип того, на что указывает объект типа *IterT». В этом предложении объявлена локальная переменная (temp) того же типа, что и объекты, на которые указывает IterT, а затем она инициализирована значением, на которое указывает iter. Если IterT будет типа vector<int>::iterator, то temp будет иметь тип int. Если же IterT будет типа vector<string>::iterator, то temp будет иметь тип string. Поскольку std::iterator_traits<IterT>::value_type – это вложенное зависимое имя типа (value_type вложено внутрь iterator_traits<IterT>, а IterT – параметр шаблона), мы должны предварить его словом typename.
Если вам неприятно даже видеть выражение std::iterator_traits<IterT>::value_type, представьте, каково набирать его на клавиатуре. Если вы, как и большинство программистов, считаете, что набрать такое более одного раза немыслимо, определите псевдоним для этого типа посредством typedef. Для имен членов классов-характеристик, к каковым относится value_type, (см. в правиле 47 информацию о классах-характеристиках), принято соглашение, согласно которому имя typedef должно совпадать с именем члена. Таким образом, определение локального typedef обычно выглядит так:
template <typename IterT>
void workWithIterator(IterT iter)
{
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
...
}
Многих программистов соседство typedef и typename поначалу раздражает, но это логическое следствие из правила обращения к вложенным зависимым именам типов. Вы скоро привыкнете. К тому же у вас есть на то веские причины. Сколько раз вы готовы напечатать std::iterator_traits<IterT>::value_type?
В качестве заключительного замечания я должен упомянуть, что не все компиляторы настаивают на строгом выполнении правил, касающихся ключевого слова typename. Некоторые принимают код, в котором typename требуется, но пропущено; некоторые принимают код, где typename присутствует, но не допускается; и некоторые (обычно это касается старых компиляторов) отвергают typename даже там, где оно необходимо. Это значит, что взаимосвязи между typename и вложенными зависимыми имен типов могут стать причиной некоторых не очень серьезных ошибок при переносе программ на другую платформу.
Что следует помнить
• В объявлениях параметров шаблона ключевые слова class и typename взаимозаменяемы.
• Используйте typename для идентификации вложенных зависимых имен типов, если они не встречаются в списке базовых классов или в качестве идентификатора базового класса в списках инициализации членов.
Данный текст является ознакомительным фрагментом.