泛型编程中,对函数使用模板。

1. 基本概念

函数模板的格式如下所示:

template<typename T> 
T max (T a, T b) 
{     
    return b < a ? a : b; 
}

含义是一目了然的事情,但是需要注意:

  • 传入的ab必须支持运算符<
  • ab必须是可以拷贝的,否则没法返回

具体的使用实例如下:

template<typename T>
T max (T a, T b)
{
    return b < a ? a : b;
}

int main()
{
    int i = 42;
    std::cout << ::max(1, i) << '\n';
    double f1 = 3.14;
    double f2 = -3.14;
    std::cout << ::max(f1, f2) << '\n';
    std::string s1 = "mathematics";
    std::string s2 = "math";
    std::cout << ::max(s1, s2) << '\n';
}

需要注意两点:

  • 传入的参数类型必须一致,否则会报错,比如::max(5.5,1)解决这个问题可以用::max(5.5,1)进行隐式转化
  • 最好使用::标识符,确保函数max()是属于全局空间,避免和std::max()混淆

我们在编译时,把模板T推导为int的过程叫做实例化(instantiation),而int这个具体的类型就叫做模板的实例(instance)

同时需要注意void也是合法的模板参数之一!

template<typename T> 
T foo(T*) { } 
void* vp = nullptr; 
foo(vp);     // OK: deduces void 
foo(void*)

另外一个很重要的点是模板的双重编译(Two-phase Translation),实际上模板在编译检查时经历了两个阶段:

  1. 在没有实例化时,称为定义时(definition time)。这时编译器会主要检查语法错误,未定义行为,静态声明等等问题。
  2. 实例化时(instantiation time),编译器会将模板实例带入重新检查一遍是否合法。

2. 参数推断

这一部分原书讲的比较简单,其实”Effective Modern C++”讲的挺好的,我之前也做了笔记,这里我直接照搬那本书上的内容。

在做之前我们先明确几个术语,下面的示范代码包含了:

  • param,形参
  • paramtype,形参的类型,这里是const T&
  • expr,实参,expression的缩写
  • CV符,const,volatile
  • 引用符,&
template<typename T> 
void f(const T& param);  

int expr = 0;
f(expr); // call f with an int

然后我们分几种情况讨论:

(1)左值引用

步骤:

  1. 忽略expr中paramtype包含的部分得到T
  2. 将T与paramtype配对的到param
template<typename T> void f(T& param);       // param is a reference

int x = 27;             // x is an int 
const int cx = x;       // cx is a const int 
const int& rx = x;      // rx is a reference to x as a const int

f(x);     // T is int, param's type is int&
f(cx);    // T is const int,                     
        // param's type is const int&
f(rx);    // T is const int,                     
        // param's type is const int&

//////////////////////////////
template<typename T> void f(const T& param);  // param is now a ref-to-const
int x = 27;              // as before 
const int cx = x;        // as before 
const int& rx = x;       // as before
f(x);     // T is int, param's type is const int&
f(cx);     // T is int, param's type is const int&
f(rx);     // T is int, param's type is const int&

////////////////////////////////////
//指针也适用这个原则
template<typename T> void f(T* param);        
// param is now a pointer
int x = 27;                 // as before 
const int *px = &x;         // px is a ptr to x as a const int
f(&x);    // T is int, param's type is int*
f(px);  // T is const int,               
        // param's type is const int*

(2)右值引用

步骤:

  1. 如果expr是左值,T和param都被推为左值并加上引用(不管有没有统统加上引用符)
  2. 如果是右值,T不变,param加上&&。

示例:

template<typename T> void f(T&& param);      
int x = 27;              // as before 
const int cx = x;        // as before 
const int& rx = x;       // as before
f(x);                   // x is lvalue, so T is int&, 
                        // param's type is also int&
f(cx);                  // cx is lvalue, so T is const int&,   
                        // param's type is also const int&
f(rx);                  // rx is lvalue, so T is const int&,   
                        // param's type is also const int&
f(27);                  // 27 is rvalue, so T is int,           
                        // param's type is therefore int&&

(3)值传递

这就是最基本的值传递。意味着,param复制了一个传入的参数。他的哲学就是忽略忽略再忽略:

  • 如果expr是引用,忽略引用的部分
  • 如果忽略引用后exprconst类型或volatile类型,忽略。
template<typename T> void 
f(T param);         // param is now passed by value 

int x = 27;          // as before 
const int cx = x;    // as before 
const int& rx = x;   // as before
f(x);                // T's and param's types are both int
f(cx);               // T's and param's types are again both int
f(rx);               // T's and param's types are still both int

注意:只是忽略引用,指针还是不变

template<typename T> void f(T param);         
// param is still passed by value
const char* const ptr ="Fun with pointers";  
// ptr is const pointer to const object  
f(ptr);
//T is const char* const

(4)退化

数组类型和指针类型是完全不一样的(虽然他们在使用时可以混用)。由于C语言老祖宗的继承关系,C++依然保留了这些特性,导致很多人误以为数组和指针参数是一样的。在传递过程中,数组会退化为它第一个元素的指针

template<typename T> void f(T param);
const char name[] = "J. P. Briggs";  // name's type is const char[13]
const char * ptrToName = name;       // array decays to pointer

f(name); // name is array, but T deduced as const char*

这种退化导致有用的信息(数组长度)丢失。然而声明引用,可以使得模板推断保留成数组的形式

template<typename T>
void f(T& param);
f(name);  //deduce to const char[13]

在这个例子中T被推导为const char[13]param则被推导为const char(&)[13]如果改为T&&Tparam都被推为const char(&)[13]

讲完数组,再来讲讲函数。在C++中不止是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组的全部讨论都可以应用到函数来:

void someFunc(int, double);  // someFunc is a function;
                             // type is void(int, double)
template<typename T> void f1(T param);     // in f1, param passed by value
template<typename T> void f2(T& param);    // in f2, param passed by ref
f1(someFunc);               // param deduced as ptr-to-func;
                            // type is void (*)(int, double)
f2(someFunc);               // param deduced as ref-to-func;
                            // type is void (&)(int, double)

3. 多模板参数

多模板参数的形式是简单的,但细节需要考究。

template<typename T1, typename T2>
T1 max (T1 a, T2 b)
{
    return b < a ? a : b;
}

auto m = ::max(1, 3.14); // 返回类型由第一个实参决定

上面的例子如果改为单模板参数则无法成功匹配。

而如果改为多模板参数,则调用过程发生了隐式转化,返回类型由第一个参数决定。如果我们把1和3.14调换位置,则返回的是double类型而不是int。

我们可以通过指定返回参数模板来解决这个问题。这段代码我们用尖括号指定模板类型,对应的位置就是RT,这样我们就能顺利返回我们想要的类型了。

template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b);
{
    return b < a ? a : b;
}

::max<double>(1, 3.14); // 返回类型为double, T1和T2被推断

在现代CPP体系中,我们可以自动推导返回类型。下面这段代码展示了11和14的一些区别。当我们调用::max(5, 1.2)的时候,5被隐式转化为double类型,返回的结果也是double类型。

template<typename T1, typename T2>
// FOR c++14
auto max (T1 a, T2 b)
{
    return b < a ? a : b;
}
//FOR c++11
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(b < a ? a : b)
{
    return b < a ? a : b;
}

但是,有时候这有可能会导致返回引用类型,因为paramtype可能就写为引用类型,我们需要调用decayed去除引用符号(需要头文件

template<typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(true ? a : b)>::type
{
    return b < a ? a : b;
}

现代CPP中引入了Common Type这一概念,相当于最大兼容。

common_type<int, float>::type // float,因为int可以转换成float
common_type<int, float, double>::type // double,因为int, float都可以转换成double

利用这个特性,我们能够进一步改造模板。(注意一下14那个有_t,11那个没有)

// for C++14
std::common_type_t<T1,T2> max(T1 a,T2 b)
{
    return a>b?a:b;
}
// for C++11
std::common_type<T1,T2>::type max(T1 a,T2 b)
{
    return a>b?a:b;
}

4. 函数模板的重载

具有相同名称的非函数模板可以和函数模板共存,利用这个特性我们能实现模板函数的重载。

int max (int a, int b)
{
    return b < a ? a : b;
}

template<typename T> //共存
T max (T a, T b)
{
    return b < a ? a : b;
}
int main()
{
    ::max(1, 42); // 调用非模板的函数
    ::max(1.0, 3.14); // 通过推断调用max<double>
    ::max('a', 'b'); // 通过推断调用max<char>
    ::max<>(1, 42); // 通过推断调用max<int>
    ::max<double>(1, 42); // 调用max<double>,不推断
}

上面的代码展示了编译器的特性:选择最优匹配的函数

重载模板的原则是需要保证只有一个合理匹配,否则会造成歧义,导致无法通过编译。下面的代码中,由于double可以被隐式转化为int,所以会导致歧义。

template<typename T1, typename T2>
auto max (T1 a, T2 b){return b < a ? a : b;}

template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b){return b < a ? a : b;}

auto a = ::max(1, 3.14); // 调用第一个模板
auto b = ::max<long double>(3.14, 1); // 调用第二个模板
auto c = ::max<int>(1, 3.14); // 错误:两个模板都匹配