C1 函数模板(Function Template)

1.1函数模板初探

函数模板提供了适用于不同数据类型的函数行为,一个函数模板代表了一组函数。除了一些未指明的信息之外,他们看起来就是一组普通的函数。


1.1.1定义模板

#include<iostream>
template <typename T>
T max(T &a, T &b){
    std::cout << "使用了何旭编写的max函数" << std::endl;
    return a > b ? a : b;
}

T 是参数名 typename 和 class 的效果是一样的,用哪个都可以。


1.1.2使用模板

#include "max1.hpp"
#include <iostream>
#include <string>

int main(){
    int a = 42;
    int b = 7;
    std::cout << ::max(a, b) << std::endl; // 这里使用作用域限制符 :: ,否则可能会调用标准库的模板
    // std::cout << max(7, i) <<std::endl;
}

模板不会被编译为支持多种类型的实体,在编译阶段,每一个用于该模板的类型的类型都会产生一个独立的实体。

具体类型取代模板类型参数的过程叫做“实例化”,他会产生一个模板实例。

每次实例化,就等于定义了新的函数。模板在具体用于某个类型的时候,才会被针对这个类型实例化。


1.1.3 两阶段编译检查(Two-Phase Translation)

模板在实例化的过程中,如果模板参数类型不支持所有模板中用到的操作符,将会遇到编译期错误。比如:

std::complex<float> c1, c2; // 这里没有提供小于运算符
...
::max(c1, c2) // 编译期ERROR

模板实例化的错误出现在编译阶段,而不是定义阶段,这是因为模板的编译是分两步进行的:

  1. 在模板定义阶段,模板的检查并不包含类型参数的检查,只包含下面几个方面:
  • 语法检查
  • 使用了为定义的不依赖于模板参数的名称
  • 未使用模板参数的 static assertions
  1. 模板在实例化阶段,为确保所有代码都是有效的,会再次检查模板,尤其是那些依赖与类型参数的部分

     template<typename T>
     void foo(T t){
         undeclared();  // 如果 undeclared() 未定义,第一阶段就会出错,因为和模板参数无关。
         undeclared(t); // 如果 undeclared(t) 未定义,第二阶段会出错,因为和模板参数有关。
         static_assert(sizeof(int) > 10, "int too small"); //与模板参数无关,肯定报错。
         static_assert(sizeof(T) > 10, "T too small"); // 与模板参数有关,第二阶段报错。
     }
    

    有些编译器并不会执行第一阶段的所有检查,因此如果模板没有被至少实例化一次的话,有些错误可能一直都不会被发现。


1.1.4 编译和链接

两阶段的编译检查给模板处理带来这样一个问题:当实例化一个模板的时候,编译器需要知道模板的完整定义。这样有悖于编译和链接分离的思想,因为函数在编译阶段只需要正确声明就好了。后面会详细讨论。目前暂时采取最简单的方法,将模板的实现写在头文件里。


1.2 模板参数推断

当我们调用自己写的max函数来处理变量的时候,模板参数将由被传递的调用参数决定。如果传两个int,C++编译器就会将T推断为int。
不过T可能只是实际传递的参数类型的一部分。比如我们定义了如下接受常量引用作为 函数参数的模板:

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

此时如果我们传递int类型的调用参数,由于调用参数和 int const& 匹配,类型参数T将被替换为T。


类型推断中的类型转换

在类型推断的时候,自动的类型转换是受到限制的:

  • 如果调用参数是按引用传递的,那么任何类型转换都不被允许,通过模板类型参数T定义的两个参数,他们的实参类型必须完全一样。

  • 如果调用参数是按值传递的,那么只有退化(decay)这一类的简单操作是被允许的,const 和 volatile 限制符会被忽略,引用会被转换成被引用类型,raw array和函数被转换为相应的指针类型。通过模板类型参数T定义的两个参数,他们实参的类型在退化后必须一样。

      template<typename T>
      T max(T a, T b);
      ...
      int const c = 42;
      int i = 1;
      max(i, c); // OK, T推断为int,const退化
      max(c, c); // OK,T推断为int
      int& ir = i;
      max(i, ir);// OK,T推断为int,ir中的引用退化
      int arr[4];
      foo(&i. arr);// OK,T推断为int*
    
      max(4, 7.2); // ERROR, T不知道自己为int还是double
      std::string s;
      foo("hello", s);// ERROR,T不知道自己是const [6]还是string
    

通过两种方法解决上面的错误;

  1. 对参数做类型转换

     max(static_cast<double>(4), 7.2);
    
  2. 显示指出T的类型,这样编译器就不去做类型推导。

     max<double>(1, 7,2);
    
  3. 调用参数有不同类型(多模板参数)


对默认调用参数的类型推断

类型推断不适用于默认调用参数,例如:

template<typename T>
void f(T = "");
f(1); // OK,T推断为int
f(); // ERROR,无法推断T的类型

为了应对这一情况,需要给模版类型也声明一个默认参数

template<typename T = std::string>
void f(T = "");

1.3 多个模板参数

之前已经学习到了和模板有关的两组参数:

  1. 参数模板,就是在template中尖括号里的
  2. 调用参数,定义在函数模板名后圆括号里的

模板参数可以是一个也可以是多个,比如定义一个max函数模板,可以接受两个不同类型的参数。

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

这样会出现一个问题:使用了其中七个类型参数的类型作为返回类型,返回值就可能会发生类型转换。

c++给出的解决方案:

  1. 引入第三个模板参数
  2. 让编译器找出返回类型
  3. 将返回类型定义为两个参数类型的“公共类型”

下面对这三种方案进行讨论:


1.3.1 作为返回类型的模板参数

根据之前讨论,模板类型推断允许我们像调用普通函数一样调用函数模板,可以不去显式的指出模板参数的类型。

当模板参数和调用参数之间没有必然的联系,且模板参数不能确定的时候,就要显式地致命4模板参数。比如可以引入第三个模板参数来制定返回类型

template<typename T1, typename T2, typename RT>
RT max(T1 a, T2 b){
    ...
}

到目前为止我们看到的情况是:

要不所有模板都被显式指定,要么一个都不指定。

另外一种方法是只指定第一个模板参数的类型,其余参数由推断获得。

通常而言,我们必须显示制定所有模板参数的类型,知道某一个模板参数的类型可以背推断出来为止。


1.3.2 返回类型推断

如果函数的返回类型是由模板参数决定的,那么判断返回类型最简单最好的方法就是让编译器来做这件事。从C++14开始,就不需要把返回类型生命为任何模板参数类型。(只需要声明一个auto)

template<typename T1, typename T2>
auto max(T1 a, T2 b){
    ...
}

在 C++14 之前,要想让编译器推断出返回类型,就必须让或多或少的函数实现成为函数声明 的一部分。在 C++11 中,尾置返回类型(trailing return type)允许我们使用函数的调用参数。 也就是说,我们可以基于运算符? :的结果声明返回类型:

template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b<a?a:b>)
{
    ...
}

返回类型是由运算符? :的结果决定的, 这虽然复杂但是可以得到想要的结果。

但是也可能有严重的问题:由于T可能是引用类型,返回类型也可能被判断为引用,所以应该返回的是decay后的T:

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

在这里用到了类型萃取(type trait)std::decay<>,它返回type成员作为目标类型,定义在<type_train>中。由于其type成员是一个类型,为了获取其结果,需要用一个关键字typename修饰这个表达式。

请注意:

在这里请注意,在初始化 auto 变量的时候其类型总是退化之后了的类型。当返回类型是 auto 的时候也是这样。

在使用auto a = some_function(); 的时候,a的类型会被decay。

int i = 42; 
int const& ir = i; // ir 是 i 的引用 
auto a = ir; // a 的类型是 it decay 之后的类型,也就是 int

1.3.3 将返回类型生命为公共类型(Common Type)

从C++11开始,标准库提供了一种指定“更一般类型“的方式

std::common_type<>::type

产生的类型是他的两个模板参数的公共类型,例如:

#include<type_traits>
template<typename T1, typename T2>
std::common_type_t<T1, T2> max(T1 a, T2, b){
    return b < a ? a : b;
}

同样的, std::common_type 也是一个类型萃取(type trait), 定义在<type_traits>中,它返 回一个结构体,结构体的 type 成员被用作目标类型。因此其主要应用场景如下:

typename std::common_type<T1,T2>::type

C++11后,这个方法被简化,::type和typename可以被省略,只需要在后面加一个_t,简化后的版本:

std::common_type_t<T1,T2> // equivalent since C++14

1.4 默认模板参数

可以给模板参数指定默认值,甚至可以根据其前面的模板参数来决定自己的类型。

方法1: 我们可以直接使用运算符? :

#include<type_traits>
template<typename T1, typename T2, typename RT = std::decay_t<decltype(true? T1(): T2())>>
RT max(T1 a, T2 b){
    return b < a ? a: b;
}

这里我们用到了 std::decay_t<>来确保返回的值不是引用类型。

方法2: 我们也可以利用类型萃取std::common_type<>作为返回类型的默认值:

#include<type_traits>
template<typename T1, typename T2, RT = std::common_type_t<T1, T2>>
RT max (T1 a, T2 b) {
    return b < a ? a : b; 
}

在这里 std::common_type<>也是会做类型退化的, 因此返回类型不会是引用。

在以上两种情况下,作为调用者,你即可以使用 RT 的默认值作为返回类型:

auto a = ::max(4, 7.2);

也可以显式的指出所有的模板参数的类型:

auto b = ::max<double,int,long double>(7.2, 4);

但是,我们再次遇到这样一个问题:为了显式指出返回类型,我们必须显式的指出全部三个模板参数的类型。因此我们希望能够将返回类型作为第一个模板参数,并且依然能够从其它两个模板参数推断出它的类型。

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

基于这个定义,你可以这样调用:

int i; 
long l; 
…
max(i, l); // 返回值类型是 long (RT 的默认值) 
max<int>(4, 42); //返回 int,因为其被显式指定

1.5 函数模板的重载

就像普通函数一样,函数模版也是可以被重载的。

也就是说,可以定义多个具有相同函数名的函数。

实际调用的时候,C++编译器来决定具体调用哪个函数

函数模板重载的样例:

// maximum of two int values: 
int max (int a, int b) {
    return b < a ? a : b; 
} 

// maximum of two values of any type: 
template<typename T>
T max (T a, T b) {
    return b < a ? a : b; 
}

int main() {
    ::max(7, 42); // calls the nontemplate for two ints 
    ::max(7.0, 42.0); // calls max<double> (by argument deduction) 
    ::max(’a’, ’b’); //calls max<char> (by argument deduction) 
    ::max<>(7, 42); // calls max<int> (by argumentdeduction) 
    ::max<double>(7, 42); // calls max<double> (no argumentdeduction) 
    ::max(’a’, 42.7); //calls the nontemplate for two ints
}

根据上面的代码可知,一个非模板函数可以和一个与其同名的函数模板共存,并且这个同名函数模板可以被实例化为与非模板函数具有相同类型的调用参数。在所有其他因素相同的情况下,模板解析将优先选择非模板函数,而不是用模板进行实例化。

::max(7, 42); // both int values match the nontemplate function perfectly

如果模板可以实例化出一个更匹配的函数,那么就会选择这个模板。

::max(7.0, 42.0); // calls the max<double> (by argument deduction) 
::max(’a’, ’b’); //calls the max<char> (by argument deduction)

也可以显式指定一个空的模板列表。这表明它会被解析成一个模板调用,其所有的模板参 数会被通过调用参数推断出来:

::max<>(7, 42); // calls max<int> (by argument deduction)

由于在模板参数推断时不允许自动类型转换,而常规函数是允许的,因此最后一个调用会 选择非模板参函数(‘a’和 42.7 都被转换成 int):

::max(’a’, 42.7); //only the nontemplate function allows nontrivial conversions

一个有趣的例子是我们可以专门为 max()实现一个可以显式指定返回值类型的模板:

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; 
}

现在可以这样来调用max():

auto a = ::max(4, 7.2); // uses first template 
auto b = ::max<long double>(7.2, 4); // uses second template

如果这样调用:

auto c = ::max<int>(4, 7.2); // ERROR: both function templates match

两个模板都匹配,这样解析的时候不知道该用哪个,就会出错误。

因此,在写模板时,要保证对任意一个调用,都只会有一个模板匹配

下面是一个为指针和C字符串重载max模板:

#include <cstring> 
#include <string> 

// maximum of two values of any type: 
template<typename T> 
T max (T a, T b) {
    return b < a ? a : b; 
}

// maximum of two pointers: 
template<typename T> 
T* max (T* a, T* b) {
    return *b < *a ? a : b; 
}

// maximum of two C-strings:
char const* max (char const* a, char const* b) {
    return std::strcmp(b,a) < 0 ? a : b; 
}

int main () {
    int a = 7; 
    int b = 42; 
    auto m1 = ::max(a,b); // max() for two values of type int 
    std::string s1 = "hey"; 
    std::string s2 = "you"; 
    auto m2 = ::max(s1,s2); // max() for two values of type std::string 
    int* p1 = &b; 
    int* p2 = &a; 
    auto m3 = ::max(p1,p2); // max() for two pointers 
    char const* x = "hello";
    char const* y = "world"; 
    auto m4 = ::max(x,y); // max() for two C-strings
}

上面的重载模板中,调用参数都是按值传递的。使用模板应该尽量少改动,只是改变模板参数的个数或者显式指定某些模板参数。

还要确保函数模板在调用的时候,其已经在前方定义。


1.6 难道,我们不应该?

即使是最简单的函数模板。也会导致比较多的问题,有三个比较值得讨论的问题。


1.6.1 按值传递还是按引用传递

通常而言,建议使用按引用传递除简单类型以外的类型,这样可以减小不必要的拷贝成本。

但是出于一下原因,按值传递有时候会更好:

  • 语法简单
  • 编译器可以更好优化
  • 移动语义通常是拷贝成本更低
  • 某些情况下没有拷贝或者移动操作

再有就是,对于模板编程,具有一些特殊情况:

  • 模板既可以用于简单类型,也可以用于复杂类型,因此如果默认选择适合于复杂类型可能方式,可能会对简单类型产生不利影响。
  • 作为调用者,你通常可以使用 std::ref()和 std::cref()来按引用传递参数。
  • 虽然按值传递 string literal 和 raw array 经常会遇到问题,但是按照引用传递它们通常只会遇到更大的问题

1.6.2 为什么不适用 inline

通常而言,函数模板不需要被声明成 inline。不同于非 inline 函数,我们可以把非 inline 的函 数模板定义在头文件里,然后在多个编译单元里 include 这个文件。

唯一一个例外是模板对某些类型的全特化,这时候最终的 code 不在是“泛型”的(所有的 模板参数都已被指定)。

严格地从语言角度来看,inline 只意味着在程序中函数的定义可以出现很多次。


1.6.3 为什么不用constexpr?

从 C++11 开始,你可以通过使用关键字 constexpr 来在编译阶段进行某些计算。对于很多模 板,这是有意义的。

比如为了可以在编译阶段使用求最大值的函数,你必须将其定义成下面这样:

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

如此你就可以在编译阶段的上下文中,实时地使用这个求最大值的函数模板:

int a[::max(sizeof(char),1000u)];

或者指定 std::array<>的大小:

std::array<std::string, ::max(sizeof(char),1000u)> arr;

在这里我们传递的 1000 是 unsigned int 类型,这样可以避免直接比较一个有符号数值和一个 无符号数值时产生的警报。


1.7 总结

  • 函数模板定义了一组适用于不同类型的函数。
  • 当向模板函数传递变量时,函数模板会自行推断模板参数的类型,来决定去实例化出那种类型的函数。
  • 你也可以显式的指出模板参数的类型。
  • 你可以定义模板参数的默认值。这个默认值可以使用该模板参数前面的模板参数的类型,而且其后面的模板参数可以没有默认值。
  • 函数模板可以被重载。
  • 当定义新的函数模板来重载已有的函数模板时,必须要确保在任何调用情况下都只有一个模板是最匹配的。
  • 当你重载函数模板的时候,最好只是显式地指出了模板参数得了类型。 确保在调用某个函数模板之前,编译器已经看到了相对应的模板定义。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值