模板基础知识4——《C++程序设计语言(第四版)》第26章 实例化 笔记

模板实例化

何时需要实例化

当定义模板类时,除非程序中真正需要其定义,否则它是不会实例化的。例如:

template<typename T>
class Link {
    Link *suc; //不需要Link的定义
    //...
};

Link<int> *pl; //不需要Link的实例化

Link<int> lnk; //现在我们需要实例化Link

对于一个模板函数,只有当它真正被使用时,才需要一个函数实现来实例化它。“被使用”的含义是“被调用或被获取地址”。特别是,实例化一个类模板并不意味着要实例化它的所有成员函数。考虑如下代码

template<typename T>
class List {
    //...
    void sort();
};

class Glob {
    //...无比较运算符...
};

void f(List &lb, List<string> &ls)
{
    ls.sort();
    //...使用lb上的操作,但不包括lb.sort()...
}

其中List::sort()被实例化了,但List::sort()未被实例化。

手工控制实例化

显示实例化

显示实例化就是在一个特例化声明上加上关键字template前缀(template后面没有<)

template class vector<int>; //类
template int& vector<int>::operator[](int); //成员函数
template int convert<int, double>(double); //非成员函数

与模板函数调用类似,我们可以忽略从函数实参推断出的模板实参,例如

template int convert<int, double>(double); //正确(冗余的)
template int convert<int>(double); //正确
显示不实例化

使用extern template来实现

#include "MyVector.h"

extern template class MyVector<int>; //禁止隐式实例化,在其他某处显式实例化

void foo(MyVector<int> &v)
{
    //...在这里使用vector...
}

“其他某处”的代码可能像下面这样:

#include "MyVector"

template class MyVector<int>; //在此编译单元中实例化;使用此实例化点

名字绑定

bool tracing;

template<typename T>
T sum(std::vector &v)
{
    T t{};
    if (tracing)
        cerr << "sum(" << &v << ")\n";
    for(int i = 0; i != v.size(); i++)
        t = t + v[i];
    return t;
}
//...

#include

void f(std::vector &v)
{
    Quard c = sum(v);
}

C++语言将模板定义中使用的名字分为以下两类
[1]依赖性名字;依赖于模板参数的名字,如上面的+,在实例化点完成绑定
[2]非依赖性名字:不依赖于模板参数的名字,如上面的vector、tracing,在定一点完成绑定

依赖性名字

函数调用依赖一个模板参数当且仅当满足下列条件:
[1]根据类型推断规则,函数实参的类型依赖于一个模板参数T,例如f(T(1))、f(t)、f(g(t))及f(&t),假定t的类型是T。
[2]根据类型推断规则,函数有一个参数依赖于T,例如,f(T)、f(list&)及f(const T*)

template<typename T>
T f(T a)
{
    return g(a); //正确:a是一个依赖性名字,因此g也是
}

class Quard {/*...*/};
void g(Quard);

int z = f(Quard{2}); //f的g绑定到g(Quard)

下面的名字不是依赖性的,会产生编译错误

class Quard{/*...*/};

template<typename T>
T ff(T a)
{
    return gg(Quard{1}); //错误:作用域中没有gg(),gg(Quard{1}})并不依赖于T    
}

int gg(Quard);

int zz = ff(Quard{2});

默认情况下,编译器假定依赖性名字不是类型名。因此,为了使依赖性名字可以是一个类型,你必须用关键字typename显式说明,如:

template<typename Container>
void fct(Container &c)
{
    Container::value_type v1 = c[7]; //语法错误:编译器假定value_type不是类型名
    typename Container::value_type v2 = c[9]; //正确:显式说明vlaue_type是类型
    auto v3 = c[11]; //正确:让编译器推断
}

可以引用别名来避免使用typename的尴尬

template<typename Container>
using Value_type = typename T::value_type;

template<typename Container>
void fct2(Container &c)
{
    Value_type v1 = c[7]; //正确
    //...
}

类似地,命名.(点)、->或::后面的成员模板需要使用关键字template。例如:

class Pool {
public:
    template<typename T> T* get();
    template<typename T> void release(T*);
    //...
};

template<typename Alloc>
void f(Alloc &all)
{
    int *p1 = all.get<int>(); //语法错误:编译器get是非模板名
    int *p2 = all.template get<int>(); //正确:编译器假定get()是一个模板
    //...
}

void user(Pool &pool)
{
    f(pool);
    //
}

定义点绑定

编译器将不依赖于模板实参的名字当作模板外的名字一样处理。因此,在定义点位置这种名字必须在作用域。

int x;
template<typename T>
T f(T a)
{
    ++x; //正确:x在作用域
    ++y; //错误:作用域中没有y,且y不依赖于T
    return a; //正确:a依赖于T
}

int y;

int z = f(2);

如果找到了名字的声明,则编译器就会使用这个声明,即使随后可能发现“更好的”声明也是如此。

void g(double);
void g2(double);

template<typename T>
int ff(T a)
{
    g2(2); //调用g2(double);
    g3(2); //错误:作用域中没有g3()
    g(2); //调用g(double);g(int)不在作用域
    //...
}

void g(int);
void g3(int);

int x = ff(a);

实例化点绑定

依赖性名字含义所需的上下文由模板的使用决定,这被称作此特化版本的实例化点。
对于一个函数模板而言,此位置位于包含模板使用的最近的全局作用域或名字空间作用域中,恰好在包含此次使用的声明之后,例如:

void g(int);

template<typename T>
void f(T a)
{
    g(a); //g在实例化点绑定
}
void h(int i)
{
    extern void g(double);
    f(i);
}
//f的实例化点

f的实例化点在h()之外,保证了h()中调用的g()是全局的g(int)而非局部的g(double)
下面的递归调用是合法的

void g(int);

template<typename T>
void f(T a)
{
    g(a); //g在实例化点绑定
    if (i) h(a - 1); //h在实例化点绑定
}

void h(int i)
{
    extern void g(double);
    f(i);
}
//f的声明点

对于一个模板类或一个类成员而言,实例化点恰好位于包含其使用的声明之前。

template<typename T>
class Container {
    vector v; //元素
    //...
public:
    void sort(); //排序元素
    //...
};

//Container的实例化点
void f()
{
    Container<int> c; //使用点
    c.sort();
}

多实例化点

在以下位置,编译器会为模板生成特例化版本
[1]任何实例化点
[2]任何编译单元的末尾
[3]或是为生成特例化而特别创建的编译单元
如果一个程序用相同的模板实参组合多次使用一个模板,则模板有多个实例化点。
如果选择不同的实例化点可能导致两种不同的含义,则程序是非法的,如下:

void f(int);

namespace N {
    class X {};
    char g(X, int);
}

template<typename T>
void ff(T t, double d)
{
    f(d); //f绑定到f(int)
    return g(t, d); //g可能绑定到g(X, int)
}

auto x1 = ff(N::X{}, 1.1); //ff;可能将g绑定到N::g(X, int),1.1窄化转换为1

Namespace N { //重新打开N声明double版本
    double g(X, double);
}

auto x2 = ff(N::X, 2.2); //ff;将g绑定到N::g(X, double);最佳匹配

模板和名字空间

当函数被调用时,即使其声明不在当前作用域中,只要它是在某个实参所在的名字空间中声明的,编译器就能找到它。编译器完成依赖性名字的绑定是通过查看以下两条来实现的:
[1]模板定义点所处作用域中的名字
[2]依赖性调用的一个实参的名字空间中的名字
例如:

namespace N {
    class A {/*...*/};
    char f(A);
}

char f(int);

template<typename T>
char g(T t)
{
    return f(t); //选择依赖于T的实参的f()
}

char f(double);

char c1 = g(N::A()); //导致N::f(N::A)
char c2 = g(2); //导致f(int)被调用
char c3 = g(2.1); //导致f(int)被调用;f(double)不会被考虑

注意,g(2.1)的调用f(int),是因为在模板声明定义时找不到下面的char f(double);

过于激进的ADL

实参依赖查找(ADL)对避免冗长代码很有用

#include 
int main()
{
    std::cout << "Hello, world" << endl; //正确,因为使用了ADL
}

与未受限模板组合使用时,ADL可能显得“过于激进”了。如下

#include
#include

namespace User {
    class Customer {/*...*/};
    using Index = std::vector;

    void copy(const Index&, Index&, int deep);

    void algo(Index& x, Index& y)
    {
        //...
        copy(x, y, false); //错误
    }
}

由于vector定义在std中,最终在中,找到了

template<typename In, typename Out>
Out copy(In, In, Out);

可通过显示指定来解决该问题,例如:

void User::algo(Index& x, Index& y)
{
    User::copy(x, y, false); //正确
    std::swap(*x[i], *x[j]); //正确:只会考虑std::swap
}

来自基类的名字

依赖模板参数的基类
void g(int);

struct B {
    void g(char);
    void h(char);
};

template<typename T>
class X : public T {
public:
    void f()
    {
        g(2); //调用g(int)
    }
    //...
};

void h(X x)
{
    x.f();
}

g(2)调用了g(int),如果想让它调用T的可以有三种方法
[1]用依赖性类型限定名字,如T::g
[2]声明一个名字指向此类的一个对象,如this->g
[3]用using声明将名字引入作用域,如using T::g
例如:

void g(int);
void g2(int);

struct B {
    using Type = int;
    void g(char);
    void g2(char);
};

template<typename T>
class X : public T {
public:
    typename T::Type m; //正确
    Type m2; //错误(Type不在作用域中)

    using T::g2(); //将T::g2()引入作用域

    void f()
    {
        this->g(2); //调用T::g
        g(2); //调用::g(int)!!!
        g2(2); //调用T::g2
    }
    //...
};

void h(X x)
{
    x.f();
}

你可能感兴趣的:(C++,c++)