Эта статья является продолжением первой части про шаблоны и шаблонные функции в C++.
Шаблонные функции-члены
Функции-члены класса тоже могут быть шаблонными. Например, у нас имеется класс Math
со статической функцией abs
, которая вычисляет абсолютное значение числа:
struct Math
{
static int abs ( int value )
{
return (value<0?-value:value) ;
}
} ;
Эта реализация только для типа int
, а ведь параметры могут быть и других типов. Делать шаблонным весь класс не имеет смысла, поэтому мы сделаем шаблонной только функцию-член:
#include <iostream>
struct Math
{
template < typename T >
static T abs ( const T & value )
{
return (value<0?-value:value) ;
}
} ;
int main()
{
std::cout << Math::abs(-3.67) << std::endl ;
}
Как видите, всё просто. Шаблонными могут быть не только статические функции. Но необходимо учесть, что виртуальные функции не могут быть шаблонными:
struct my_class
{
template < typename T>
virtual void foo() {} //<-- Ошибка
} ;
Также шаблонными могут быть операторы и конструкторы:
#include <iostream>
template < typename T >
struct my_class
{
my_class ( const T & val = T() ) : m_x (val)
{}
my_class ( const my_class & src ) : m_x ( src.m_x )
{}
my_class & operator= ( const my_class & rhv )
{
m_x = rhv.m_x ;
}
bool operator== ( const my_class & rhv )
{
return m_x == rhv.m_x ;
}
T getX() const
{
return m_x ;
}
private:
T m_x ;
} ;
int main()
{
my_class<int> obj1(6) ;
my_class<short> obj2 (6) ;
std::cout << obj1.getX() << std::endl ;
std::cout << obj2.getX() << std::endl ;
}
my_class
имеет один существенный недостаток — это объекты абсолютно разных типов. Если мы попробуем сравнить объекты obj1
и obj2
, то получим ошибку времени компиляции, ведь как эти объекты сравнивать компилятору неизвестно.
Конечно, можно сравнить результаты вызова функции getX
, но это всё равно не решает всех проблем — нельзя присвоить один объект другому или сконструировать один объект из другого(если их типы разные).
Чтобы выйти из ситуации, мы определим шаблонные версии конструкторов и операторов.
#include <iostream>
template < typename T >
struct my_class
{
//#1
template < typename U > friend class my_class ;
template < typename U >
my_class ( const U & val = U() ) : m_x (val)
{}
//#2
my_class ( const my_class & src ) : m_x ( src.m_x )
{}
//#3
template < typename U >
my_class ( const my_class<U> & src ) : m_x ( src.m_x )
{}
my_class & operator= ( const my_class & rhv )
{
m_x = rhv.m_x ;
}
template < typename U >
my_class & operator= ( const my_class<U> & rhv )
{
m_x = rhv.m_x ;
}
template < typename U >
bool operator== ( const my_class<U> & rhv )
{
return m_x == rhv.m_x ;
}
T getX() const
{
return m_x ;
}
private:
T m_x ;
} ;
int main()
{
my_class<int> obj1(6) ;
my_class<short> obj2 (8) ;
my_class<double> obj3(obj1) ;
obj3 = obj2 ;
std::cout << (obj1 == obj2) << std::endl ;
std::cout << (obj3 == obj1) << std::endl ;
}
Как видите, теперь преобразования возможны, конечно, если возможны преобразования между объектами m_x
в самих классах, т.е. написать my_class<std::string> obj4(obj3);
в данном случае не получится, т.к. нет соответствующего преобразования из типа double в тип std::string
.
Как вы могли заметить, я убрал из класса нешаблонные конструктор и оператор сравнения, оставив только их шаблонные версии. Но можно было их и оставить, чтобы лучше организовать работу с одинаковыми типами (например, избежать накладных расходов на преобразования).
Также, заметьте, что в классе остался нешаблонный конструктор копий(#2), т.к. если его убрать, то компилятор сгенерирует его самостоятельно, т.е. шаблонный конструктор(#3) не заменяет конструктор копий(#2) и если не определить копирующий конструктор явно, то компилятор самостоятельно его сгенерирует. То же самое относится и к оператору присваивания. В C++11 это же относится еще и к move-версиям.
Также, стоит отметить, что при разных значениях аргументов шаблона у нас получаются разные типы, значит, my_class < T >
не имеет доступа к приватным данным класса my_class < U >
(и к защищенным, если не является наследником). Поэтому для обращения к приватным данным необходимо объявить эти классы друзьями (#1). Если убрать это объявление, то получим ошибку вида «m_x is private».
Из данного примера также видно, что у шаблонного класса могут быть шаблонные функции-члены. Хочется еще отметить, что у шаблонного класса могут быть виртуальные функции, но виртуальные функции не могут быть шаблонными.
Аргументы шаблона не-типы.
Аргументами шаблона могут быть не только типы. Рассмотрим небольшой пример
#include <iostream>
template < size_t N >
struct my_type
{
void foo () const
{
std::cout << N << std::endl ;
}
};
int main()
{
my_type<5> o1 ;
my_type<7> o2 ;
o2.foo() ;
o1.foo() ;
}
После выполнения получим значения 7 и 5. Эти параметры задаются на этапе компиляции и не совсем очевидно для чего это может быть нужно. Вскоре мы рассмотрим применение таких аргументов, посмотрите на шаблон класса std::bitset
, в котором аргумент не-тип задает размер множества:
std::bitset<16> bs ;
Как известно, при разных аргументах шаблона компилятор создаст разные типы. То есть my_class <int>
и my_class <double>
— это разные типы. Так же и с параметрами не-типами, т.е. my_type<5>
и my_type<7>
— это тоже разные типы. Это свойство нам тоже понадобится в дальнейшем. Но на время отвлечемся от этого.
Для начала определимся, что же можно использовать в качестве аргумента не-типа. Для этого обратимся к стандарту.
14.1/4
A non-type template-parameter shall have one of the following (optionally cv-qualified) types:
- integral or enumeration type,
- pointer to object or pointer to function,
- lvalue reference to object or lvalue reference to function,
- pointer to member,
- std::nullptr_t.
Ух ты, какой выбор. Но увы, есть и другие ограничения.
14.3.2/1
A template-argument for a non-type, non-template template-parameter shall be one of:
- an integral constant expression (including a constant expression of literal class type that can be used as an integral constant expression as described in 5.19); or
- the name of a non-type template-parameter; or
- a constant expression (5.19) that designates the address of an object with static storage duration and external or internal linkage or a function with external or internal linkage, including function templates and function template-ids but excluding non-static class members, expressed (ignoring parentheses) as & id-expression, except that the & may be omitted if the name refers to a function or array and shall be omitted if the corresponding template-parameter is a reference; or
- a constant expression that evaluates to a null pointer value (4.10); or
- a constant expression that evaluates to a null member pointer value (4.11); or
- a pointer to member expressed as described in 5.3.1.
Это еще не всё, но для начала этого достаточно. Попробуем передать в параметр шаблона ссылку на объект:
#include <iostream>
class my_class
{
int m_x ;
public:
my_class ( int x ) : m_x (x)
{
}
int getX ( ) const
{
return m_x ;
}
} ;
//Аргументом шаблона является ссылка на объект типа my_class
template < my_class & mc >
struct my_type
{
void foo () const
{
//Используем переданный в аргументе шаблона объект
std::cout << mc.getX() << std::endl ;
}
};
my_class obj1(7) ;
my_class obj2(-5) ;
int main()
{
my_type<obj1> o1 ; //o1 теперь "связан" с объектом obj1
my_type<obj2> o2 ; //o2 теперь "связан" с объектом obj2
//Вызов соответствующих функций
o1.foo() ;
o2.foo() ;
}
Для начала этой информации нам хватит.
Шаблонные параметры шаблона.
Кратко рассмотрим передачу шаблона в качестве аргумента шаблона. Создадим функцию, которая принимает в качестве аргумента бинарный предикат и ссылки на две переменные. В случае если предикат возвращает true, делаем swap переданных переменных:
#include <iostream>
#include <functional>
#include <algorithm>
template < typename BinPred , typename T >
void foo ( T & obj1 , T & obj2 , BinPred pred )
{
if ( pred(obj1,obj2) )
std::swap (obj1,obj2) ;
}
int main ()
{
int x = 10 ;
int y = 30 ;
//foo<std::less> ( x , y ) ;
foo ( x , y , std::less<int>() ) ;
std::cout << x << ' ' << y << std::endl ;
}
А теперь попробуем передать предикат std::less
как параметр шаблона.
Для этого необходимо изменить функцию foo
.
Шаблонные аргументы шаблона (template template arguments) определяются как
template < template-parameter-list > class ..._opt identifier_opt
template < template-parameter-list > class identifier_opt = id-expression
В данном случае использование ключевого слова class
— принципиально и typename его не заменит.
template < template <typename> class BinPred , typename T >
void foo ( T & obj1 , T & obj2 )
{
if ( BinPred<T>()(obj1,obj2) )
std::swap(obj1,obj2) ;
}
template < typename > class BinPred
— вот он, наш шаблонный аргумент. Как видим, у шаблонного аргумента один аргумент, его имя не имеет значения, поэтому отсутствует.
BinPred<T>()(obj1,obj2)
— здесь создаем объект класса BinPred<T>()
и вызываем у него функцию operator()
с двумя параметрами.
Теперь перепишем функцию main:
int main ()
{
int x = 10 ;
int y = 30 ;
foo<std::less> ( x , y ) ; //Для std::less не нужно указывать тип параметра, т.к. мы передаем сам шаблон
std::cout << x << ' ' << y << std::endl ;
}
Вот мы и сделали еще один крохотный шажок в изучении шаблонов.
Комментарии к статье: 9
Возможность комментировать эту статью отключена автором. Возможно, во всем виновата её провокационная тематика или большое обилие флейма от предыдущих комментаторов.
Если у вас есть вопросы по содержанию статьи, рекомендуем вам обратиться за помощью на наш форум.