类属函数 templat function

  1. 同一函数对不同类型的数据完成相同的操作
  2. 宏实现:
    1. #define max(a,b) ((a)>(b)?(a):(b))
    2. 缺陷:
      1. 只能实现简单的功能
      2. 没有类型检查

函数重载

1
2
3
int max(int,int);
double max(double,double);
A max(A,A) ;
  1. 缺陷:
    1. 需要定义的重载函数太多
    2. 定义不全
  2. 不可以只有返回值不同

函数指针

1
void sort(void * , unsigned int, unsigned int, int (* cmp) (void *, void *) )
  1. 缺陷:
    1. 需要定义额外参数
    2. 大量指针运算
    3. 实现起来复杂
    4. 可读性差
  2. template更加结构清晰,实现简单

函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//int和double都可以使用,编译器编译的并不是之下的代码,而是T转化成具体代码,然后分别编译
template <typename T>
void sort(int A[], unsigned int num) {
for(int i=1; i<num; i++)
for (int j=0; j< num - i; j++) {
if (A[j] > A[j+1]) {
T t = A[j];
A[j] = A[j+1];
A[j+1] = t;
}
}
}
class C {...}
C a[300];
sort(a, 300);//没有重载>

C++
  1. 必须重载操作符 >
  2. 函数模板定义了一类重载的函数
  3. 函数模板的实例化:
    1. 隐式实现
    2. 根据具体模板函数调用
  4. 函数模板的参数
    1. 可有多个类型参数,用逗号分隔
    2. 可带普通参数
      • 必须列在类型参数之后
      • 调用时需显式实例化,使用默认参数值可以不显式实例化
      • 可以有默认值

image.png

1
2
3
4
5
template <class T1, class T2>
void f(T1 a, T2 b) {}
template <class T, int size>
void f(T a) {T temp[size];}
f<int,10>(1);
  1. 函数模板与函数重载配合使用(编译器优先使用没有使用模板的函数)
  2. 优先匹配重载函数,再继续调用函数的显式、具体化的版本、再调用函数模板
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template <class T> T max(T a, T b) {
    return a>b?a:b;
    }
    int x, y, z;
    double l, m, n;
    z = max(x,y);
    l = max(m,n);
    //为了解决max(x,m)我们使用函数重载更新,优先调用
    double max(int a, double b) {
    return a>b? a : b;
    }

类属类

min

  1. 类定义带有类型参数,类属类需要显式实例化

  2. 类模板中的静态成员属于实例化后的类

  3. 类模板的实例化:创建对象时显式指定,指明类型参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Stack {
    int buffer[100];
    public:
    void push(int x);
    int pop();
    };
    void Stack::push(int x) {...}
    int Stack::pop(){...}

    Stack st1;
    -----------------------------------------------------------------------------
    template <class T>
    class Stack {
    T buffer[100];
    public:
    void push( T x);
    T pop();
    };
    // 声明T是类型参数
    template <class T>
    // <T> 用一个具体类型去实例化的stack,<>是用来区分不同类型的
    void Stack <T> ::push(T x) {...}

    // 装double的push和装int的push不是同一个push
    template <class T>
    T Stack <T> ::pop() {...}

    //如下是显式实例化
    Stack <int> st1;
    Stack <double> st2;

    类模板中的静态成员属于实例化后的类,不是属于模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    template <class T, int size>
    class Stack {
    T buffer[size];
    public:
    void push(T x);
    T pop();
    };

    template <class T, int size>
    void Stack <T, size>::push(T x) {...}

    template <class T, int size>
    T Stack <T, size>::pop() {...}

    Stack <int, 100 > st1 ;
    //上面改为template<class T = int,int size = 100>
    // 这里可以改成stack<> st1用来显示实例化
    Stack <double, 200 > st2 ;
  4. 类型参数也可以给出初始值,模板类如果不按照从右往左指定默认值参数,会导致编译错误

  5. 函数模板的默认值不一定是从右向左的,C++11之后函数模板才接受默认值参数。

  6. 总而言之从右向左给出默认值总是没有问题的

image.png

  1. 如果在模块A中要使用模块B中定义的某模板的实例,而在B中未使用这个实例,则模板无法使用这个实例
  2. 为什么C++中模板的完整定义常常出现在头文件中?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    //file1.h
    template <class T> class S {
    T a;
    public:
    void f();
    };

    //file1.cpp
    #include "file1.h"
    template <class T>
    void S<T>::f(){...}

    template <class T>
    T max(T x, T y){return x>y?x:y;}
    void main() {
    int a,b;
    max(a,b);//实例化函数模板
    S<int> x;
    x.f();
    }

    //file2.cpp
    #include "file1.h"
    extern double max(double,double);
    void sub(){
    max(1.1,2.2);//error
    S<float> x;
    x.f();//error
    }
    //不能通过编译,为什么?file2.cpp找不到max定义,也找不到完整的S代码
  • 因为编译器使用模板时,没有分配存储空间。只有实例化时,才会分配模板空间。所以,因为file1中没有实例化float的模板,因此file2中找不到。尝试自己实例化模板代码
  • 但是,file1中只有声明,没有实现,因此无法实例化代码
  • 所以C++中模板的完整定义常常出现在头文件中

image.png

Template MetaProgramming 元编程

  1. 元程序就是编写一些直接可以生成或者操作其他程序的程序,要在更高层次上。
  2. 编写元程序就是元编程(两级编程),在编译的时候就已经完成编程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    template<int N>
    class Fib
    {
    public:
    enum { value = Fib<N - 1>::value + Fib<N - 2>::value };
    };

    //模板显式实例化
    template<> // 需要一个template<> 标明是特殊实例化
    class Fib<0>{
    public:
    enum { value = 1 };
    };
    template<>
    class Fib<1>
    {
    public:
    enum { value = 1 };
    };
    void main() {
    cout << Fib<8>::value << endl;// calculated at compile time
    }

易错

默认参数

1
2
3
4
5
6
7
8
9
10
template<int i = 0, typename T = double>
void DefFunc2(T a) {
cout << a + i << endl;
};

void test() {
DefFunc2<10,double>(5.555); // 15.555
DefFunc2(5); // 5
DefFunc2<10>(5.6); // 5.6
}
  • 尖括号中的代表的是模板参数,圆括号中代表的是函数参数
  • 模板参数中,可以提供类型参数typename T,对类型T进行默认类型的指定T = double,也可以提供实际的参数i,提供默认值i = 0

类模板:通用的类描述(使用泛型来定义类),进行实例化时,其中的泛型再用具体的类型替换。
函数模板:通用的函数描述(使用泛型来定义函数),进行实例化时,其中的泛型再用具体的类型替换。
如果能够从函数实参中推导出类型的话,那么默认模板参数就不会被使用,反之,默认模板参数则可能会被使用。

1
2
3
4
5
6
7
8
9
10
template <class T, class U = double>
void f(T t = 0, U u = 0) {};
void g()
{
f(1, 'c'); // f<int, char>(1, 'c') 根据实参类型推出模板参数中T U分别代表了什么类型
f(1); // f<int, double>(1, 0), 使用了默认模板参数double
f(); // 错误: T无法被推导出来
f<int>(); // f<int, double>(0, 0), 使用了默认模板参数double
f<int, char>(); // f<int, char>(0, 0)
}

定义了一个函数模板ff同时使用了默认模板参数和默认函数参数。
可以看到,由于函数的模板参数可以由函数的实参推导而出:
在f(1)这个函数调用中,实例化出了模板函数的调用应该为f<int, double>(1, 0),其中,第二个类型参数U使用了默认的模板类型参数double,而函数实参则为默认值0。
类似地,f()实例化出的模板函数第二参数类型为double,值为0。
而表达式f()由于第一类型参数T的无法推导,从而导致了编译的失败。
而通过这个例子也可以看到,默认模板参数通常是需要跟默认函数参数一起使用的。


1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename R = int, typename U>
R func(U val)
{
return val;
}
int main()
{
func(97); // R=int, U=int
func<char>(97); // R=char, U=int
func<double, int>(97); // R=double, U=int
return 0;
}

  1. func(97)是根据函数的实参得出,val = 97,因此U就是int,所以R就是默认值int
  2. 此处同理可得Uint,但<>中有显式地指明了char,所以R就是char而不是默认值int

报错1

1
2
3
4
5
6
7
8
9
10
template<int i, typename T>
void DefFunc2(T a) {
cout << a + i << endl;
}; // OK 函数模板不用遵循“由右往左”的规则

void test() {
DefFunc2<10,double>(5.555); // 15.555
DefFunc2(5); // 报错了
DefFunc2<10>(5.6); // 5.6
}

此处第8行会报错,因为模板参数中i没有提供默认值了,所以需要显式的给出i的值

报错2

类模板在为多个默认模板参数声明指定默认值时,必须遵照“从右往左”的规则进行指定。
而这个规则对函数模板来说并不是必须的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename T1, typename T2 = int>
class DefClass1 {};

template <typename T1 = int, typename T2>
class DefClass2 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则

template <typename T, int i = 0>
class DefClass3 {};

template <int i = 0, typename T>
class DefClass4 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则

template <typename T1 = int, typename T2>
void DefFunc1(T1 a, T2 b) {}; // OK 函数模板不用遵循“由右往左”的规则

template <int i = 0, typename T>
void DefFunc2(T a) {}; // OK 函数模板不用遵循“由右往左”的规则

template<typename T1 = int, typename T2>
void DefFunc3(T1 a = 10, T2 b) {}; // ERROR: 函数参数需要遵循“由右往左”的规则

报错3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename T = int, int size = 0>
class Stack {
T buffer[100];
int point;
public:
Stack() : point(0) {
cout << "constructor" << size << endl;
}

void push(T val);

T pop();
}
template<typename T, int size = 0> // 报错了
void Stack<T, size>::push(T val) {...}

template<typename T = int, int size> // 报错了
void Stack<T, size>::push(T val) {...}
  1. 在类模板的外部定义类中的成员时template 后的形参表应**省略默认的形参类型 **template<typename T = int, int size>
  2. 在类模板的外部定义类中的成员时template 后的形参表应省略默认的参数值template<typename T, int size = 0>

正确的是template<typename T,int size>

小结

  1. 函数自身的默认参数需要遵循从右往左的原则
  2. 模板类中提供的默认参数需要遵循从右往左的原则
  3. 模板参数中提供的默认参数不需要遵循从右往左的原则
  4. 在类模板的外部定义类中的成员时template 后的形参表应省略默认的形参类型
  5. 在类模板的外部定义类中的成员时template 后的形参表应省略默认的参数值