C++Primer_Chap16_模板和泛型编程_List01_定义模板_笔记

  面向对象变成(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:

  • OOP能处理类型在程序运行之前都未知的情况
  • 泛型编程中,在编译时就能获知类型。

函数模板

  我们可以定义一个通用的函数模板(function template),一个函数模板就是一个公式,可生成针对特定类型的函数版本

template <typename T>
int compare( const T &val1, const T &val2)
{
    if( val1 < val2) 
        return -1;
    if( val2 < val1)
        return 1;
    return 0;
}

  模板定义以关键字template开始,后跟一个模板参数列表(template parameter list),这是一个逗号分隔的一个或多个模板参数(template parameter)的列表,用<>包围起来。

  一般来说,可以将类型参数看做类型说明符,就想内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数,以及在函数体内用于声明变量或类型转换。类型参数前必须使用关键字class或typename(在模板参数列表中,class和typename没有什么不同):

template<typename T, class U> calc(const T&, const U&);

非类型模板参数

  除了定义类型参数,还可以在模板中定义非类型参数(nontype parameter)。一个非类型参数表示一个值而非一个类型。通过一个特定的类型名而非关键字class或typename来指定非类型参数。

  当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时时实例化模板。

template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char(&p2)[M])
{
    return strcmp(p1, p2);
}

compare("hi", "mom");
int compare( const char (&p1)[3], const char (&p2)[4]);

  编译器会使用字面常量的大小代替N和M,从而实例化模板。一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数实参必须具有静态的生存期。我们不能用一个普通(非static)局部变量或动态变量作为指针或引用非类型模板参数的实参。指针参数也可以用nullptr或值为0的常量表达式来实例化。

inline和constexpr的函数模板

  inline和constexpr说明符放在模板参数列表之后,返回类型之前:

template<typename T> inline T min(const T&, const T&);

  编写泛型代码的两个重要原则:

  • 模板中的函数参数是const的引用(保证了函数可以用于不能拷贝的类型)
  • 函数体中的条件判断仅使用<比较运算(降低compare对要处理的类型的要求,只需要有<,不必同时支持>)

  实际上,如果真的关心类型无关和移植性,可能需要使用less来定义我们的函数.(弥补原版本针对两个指针,且两个指针未指向相同的数据时代码行为未定义的问题)

template<typename T> int compare( const T &v1, const T &v2)
{
    if( less<T>()(v1, v2))
        return -1;
    if( less<T>()(v2, v1))
        return 1;
    return 0;
}

  模板程序应该尽量减少对实参类型的要求。

模板编译

  当编译器遇到一个模板定义时,它并不生成代码。只有实例化出模板的一个特定版本时,编译器才会生成代码。即当我们使用而不是定义模板时,编译器才生成代码,该特性会影响如何组织代码以及错误何时被检测到。

  为了生成一个实例化版本,编译器需要掌握函数模板和类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常保护声明和定义。

  类模板

  类模板(class template)是用来生成类的狼途的。和函数模板不同之处是,编译器不能为类模板推断模板参数类型。为了使用类模板,我们必须在模板名后面的尖括号中提供额外信息——用来代替模板参数的模板实参列表

template <typename T> class Blob {
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;

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

    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
	
    void push_back( const T &t) { data->push_back(t); }
    void push_back( const T &&t) { data->push_back(std::move(t)); }
    void pop_back();
	
    T& back();
    T& operator[](size_type i);

private:
    std::shared_ptr<std::vector<T>> data;
    void check(size_type i, const std::string &msg) const;
};

  实例化类模板时必须提供额外的信息。我们现在知道浙西额额外信息是显式模板实参(explicit template argument)列表,他们被绑定到模板参数:

Blob<int> ia;
Blob<int> ia2 = {0, 1, 2, 3, 4};

 实例化会让编译器实例化出一个与下面定义等价的类:

template <> class Blob<int> {
public:
    typedef typename std::vector<int>::size_type size_type;

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

    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
	
    void push_back( const int &t) { data->push_back(t); }
    void push_back( const int &&t) { data->push_back(std::move(t)); }
    void pop_back();
	
    T& back();
    T& operator[](size_type i);

private:
    std::shared_ptr<std::vector<int>> data;
    void check(size_type i, const std::string &msg) const;
};

  一个类模板的每个实例都会形成一个独立的类。类型Blob<string>和任何其他Blob类型都没有关联,也不会对任何其他Blob类型的成员有特殊访问权限。

类模板的成员函数

  类模板的成员函数具有和模板相同的模板参数。因此,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。

ret-type StrBlob::member-name(parm-list)
{}

template <typename T>
ret-type Blob<T>::member-name(parm-list)
{}

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

template <typename T>
Blob<T>::Blob( std:: initializer_list<T> i1) : 
                data( std::make_shared<std::vector<T>>(i1))
 {}

template <typename T>
void Blob<T>::pop_back()
{
	check( 0, "back on empty Blob");
	return data->pop_back();
}

template <typename T>
T& Blob<T>::back()
{
	check( 0, "back on empty Blob");
	return data->back();
}

template <typename T>
T& Blob<T>::operator[](size_type i)
{
	check(i, "subscript out of range");
	return (*data)[i];
}

template <typename T> 
void Blob<T>::check( size_type i, const std::string &msg) const
{
	if( i >= data->size() )
		throw std::out_of_range(msg);
}

  如果一个成员函数没有被使用,则它不会被实例化。成员函数只有在被用到时才进行实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。

在类代码内简化模板类名的使用

  当我们使用一个类模板类型时必须提供模板实参,但例外是在类模板自己的作用域中,我们可以直接使用模板名而不提供实参:

template <typename T> 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];
    }
    BlobPtr& operator++();
    BlobPtr& operator--();
private:
    std::shared_ptr<std::vector<T>> check(std::size_t, const std::string&) const;	
    std::weak_ptr< std::vector<T>> wptr;
    std::size_t curr;
}; 

    前置递增/递减返回BlobPtr&而不是BlobPtr<T>&,当我们处于一个类模板的作用域中时,编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。

  在类模板外定义其成员时,必须记住并不在类的作用域中,直到遇到类名才表示进入类的作用域.

template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
    BlobPtr ret = *this;
    ++*this;
    return ret;
}

类模板和友元

  当一个类包含一个有友元声明时,类和友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板示例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以值授权给特定实例。

一对一友好关系

  类模板与另一个(类或函数)模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系。为了引用(类或函数)模板的一个特定实例,我们必须首先声明模板自身(一个模板的声明包括模板参数列表):

template <typename> class BlobPtr;
template <typename> class Blob;
template <typename T>
    bool operator==(const Blob<T>&, const Blob<T>&);

template <typename T> class Blob{
    friend class BlobPtr<T>;
    friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
}; 

  前面三行的声明是函数的参数声明和Blob中的友元声明所需要的。

  友元声明用Blob的模板形参作为它们自己的模板实参。因此,友好关系被限定在用相同类型实例化的Blob和BlobPtr相等运算符之间。

Blob<char> ca;
Blob<char>
operator==<char>

Blob<int> ia;
Blob<int>
operator==<int>

通用和特定的模板友好关系

  一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的示例为友元:

template <typename T> class Pal;

class C{

    friend class Pal<C>;    //用C实例化的Pal是C的一个友元

    //Pal2的所有示例都是C的友元,这种情况无需前置声明
    template <typename T> friend class Pal2;
};

template <typename T> class C2 {

    //C2的每个实例将相同实例化的Pal声明为友元
    friend class Pal<T>;    //Pal的模板声明必须在作用域之内

    //Pal2的所有示例都是C2的每个实例的友元,不需要前置声明
    template <typename X> friend class Pal2;

    //Pal3是一个非模板类,它是C2所有示例的友元
    friend class Pal3;
};

  为了让所有实例称为友元,友元声明中必须使用与类模板本身不同的模板参数。

令模板自己的类型参数称为友元

  可以将模板类型参数声明成友元:

template <typename Type> class Bar{
    friend Type;
};

模板类型别名

  我们可以定义一个typedef来引用实例化的类,但不能定义一个typedef引用一个模板。不过,我们可以为类模板定义一个类型别名:

typedef Blob<string> StrBlob;

template<typename T> using twin = pair<T, T>;
twin<string> authors;    //authors是一个pair<string, string>
twin<int> win_loss;       //win_loss是一个pair<int, int>
twin<double> area;

类模板的static成员

  类模板的static数据成员必须有且仅有一个定义。类模板的每个实例都有一个独有的static对象。与定义模板的成员函数类似,我们将static数据成员也定义为模板:

template <typename T> class Foo {
public:
    static std::size_t count() {return ctr;}
private:
    static std::size_t ctr;
};

template <typename T>
size_t Foo<T>::ctr = 0;


Foo<int> fi;
auto ci = Foo<int>::count();
ct = fi.count();
ct = Foo::count();    //错误

模板参数

  类似函数参数的名字,一个模板参数的名字没有什么内在含义,其可用范围是在其什么之后,至模板声明或定义结束之前(会隐藏外层作用域中声明的相同名字)。参数名不能重用,但定义中的名字不必与声明中的模板参数名字相同。

使用类的类型成员

  假定T是一个模板类型参数,当编译器遇到类似T::mem这样的代码时,它不会知道men是一个类型成员还是一个static数据成员,直到实例化。但为了处理模板,编译器必须指定名字是否表示一个类型。

T::size_type * p;
//是定义一个p的指针变量?
//还是将一个名为size_type的static数据成员与p相乘

  默认情况下,C++语言假定通过作用域访问符(::)访问的名字都不是类型。因此,如果希望使用一个模板类型参数的类型名字,必须显式告诉编译器该名字是一个类型(通过使用关键字typename来实现):

template <typename T>
typename T::value_type top(const T& c)
{    
    if( !c.empty())
        return c.back();
    else
        return typename T::value_type();
}

  当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class

默认模板实参

  我们可以提供默认模板实参(default template argument)。在C++11标准后,可以为函数和类模板提供默认实参。

template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
    if( f(v1, v2))
        return -1;
    if( f(v2, v1))
        return 1;
    return 0;
}

模板默认实参和类模板

无论何时使用一个类模板,都必须在模板名之后接上尖括号。尖括号指出类必须从一个模板实例化而来。特别,如果一个类模板为其所有模板参数都提供了默认实参,且希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:

template<class T = int> class Numbers{
    //默认T为int
public:
    Numbers(T v = 0) : val(v) {}
private:
    T val;
};

Numbers<long double> lots_of_precision;
Numbers<> average_precision;

成员模板

  一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员称为成员模板(member template)。成员模板不是虚函数。

  重载的函数调用运算符希望删除器适用于任何类型,所以将调用运算符定义为一个模板:

class DebugDelete {
public:
    DebugDelete(std::ostream &os = std::cerr ) : os(s) {}
    template <typename T> void operator()(T *p) const
    {    
        os << "deleting unique_ptr" << std::endl;
        delete p;        
    }
private:
    std::ostream &os;
};


double *p = new double;
DebugDelete d;
//调用DebugDelete::operator()(double *),释放p
d(p);

int *ip = new int;
//在一个临时DebugDelete对象上调用operator()(int *)
DebugDelete()(ip);

  由于调用一个DebugDelete对象会delete其给定指针,我们可以将其用作unique_ptr的删除器。为了重载unique_ptr的删除器,我们在尖括号内给出了删除器类型,并提供一个这种类型的对象给unique_ptr的构造函数:

unique_ptr<int, DebugDelete> p(new int, DebugDelete());
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());

类模板的成员模板

  对于类模板,也可以为其定义成员模板:

template <typename T> class Blob {
    template <typename IT> Blob(It b, It e);

};

  与类模板的普通成员函数不同,成员模板是函数模板。当在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:

template <typename T>
template <typename IT>
    Blob<T>::Blob(IT b, IT e) : 
        data(std::make_shared<std::vector<T>>(b, e) ) 
    { }

实例化和成员模板

  为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实参。与普通函数模板相同,编译器通常根据传递给成员模板的函数实参来推断它的模板类型:

int ia[] = {0,1,2};
vector<long> vi = {0, 1, 2, 3};
list<const char*> w = {"now", "is", "the", "time"};

//实例化Blob<int>类及接受两个int*参数的构造函数
Blob<int> a1(begin(ia), end(ia));

//实例化Blob<int>类及接受两个vector<long>::iterator的构造函数
Blob<int> a2(vi.begin(), vi.end());

//实例化Blob<string>类及接受两个list<const char*>::iterator的构造函数
Blob<string> a2(w.begin(), w.end());

控制实例化

当模板被使用时才进行实例化这特性意味着相同的实例可能会出现在多个对象文件中(.o)。在大系统中,在多个文件中实例化化相同模板的额外开销可能非常严重。可以通过显示实例化(explicit instantiation)来避免这种开销:

extern template declaration;    //实例化声明
template declaration;            //实例化定义

  declaration是一个类或函数声明,其中所有模板参数已被替换成模板实参,例如:

extern template class Blob<string>;        //声明
template int compare(const int&, const int&);    //定义

  由于编译器你在使用一个模板时自动对其实例化。因此extern声明必须出现在任何使用此实例化版本的代码之前。

// Application.cc
//这些模板类型必须在程序其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2;    //实例化会出现在其他位置

//Blob<int>及其接受initializer_list的构造函数在本文件中实例化
Blob<int> a1 = {0, 1, 2, 3, 4, 5, 6};
Blob<int> a2(a1);    //拷贝构造函数在本文件中实例化
int i = compare(a1[0], a2[0]);    //实例化出现在其他位置

   文件Application.o将包含Blob<int>的实例及其接受initializer_list参数的构造函数和拷贝构造函数的实例。而compare<int>函数和Blob<string>类将不再本文件中进行实例化。这些模板的定义必须出现在程序的其他文件中:

//templateBuild.cc
//实例化文件必须为每个在其他文件中声明为extern的类型和函数提供一个(非extern)的定义
template int compare(const int&, const int&);
template class Blob<string>;

   当编译器遇到一个实例化定义(与声明相对)时,它为其生存代码。因此templateBuild.o将包含compare的int实例化版本的定义和Blob<string>类的定义。

  对于每个实例化声明,在程序中某个位置必须有其显式的实例化定义。实例化定义会实例化所有成员:一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。当编译器遇到一个实例化定义时,它不了解程序使用那些成员函数。因此与处理类模板的普通实例化不同,编译器会实例化该类的所有成员。因此,我们用来显示实例化一个类模板的类型,必须能用于模板的所有的成员。

猜你喜欢

转载自blog.csdn.net/accumulating_mocai/article/details/83658465