Шаблоны в C++ — часть 2

9 комментариев

Эта статья является продолжением первой части про шаблоны и шаблонные функции в 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

Подождите, загружаются комментарии...

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

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