1.2 Template Function的基本语法

优质
小牛编辑
119浏览
2023-12-01

1.2.1 Template Function的声明和定义

模板函数的语法与模板类基本相同,也是以关键字template和模板参数列表作为声明与定义的开始。模板参数列表中的类型,可以出现在参数、返回值以及函数体中。比方说下面几个例子

template <typename T> void foo(T const& v);

template <typename T> T foo();

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

template <typename T> void foo()
{
  T var;
  // ...
}

无论是函数模板还是类模板,在实际代码中看起来都是“千变万化”的。这些“变化”,主要是因为类型被当做了参数,导致代码中可以变化的部分更多了。

归根结底,模板无外乎两点:

  1. 函数或者类里面,有一些类型我们希望它能变化一下,我们用标识符来代替它,这就是“模板参数”;

  2. 在需要这些类型的地方,写上相对应的标识符(“模板参数”)。

当然,这里的“可变”实际上在代码编译好后就固定下来了,可以称之为编译期的可变性。

这里多啰嗦一点,主要也是想告诉大家,模板其实是个很简单的东西。

下面这个例子,或许可以帮助大家解决以下两个问题:

  1. 什么样的需求会使用模板来解决?

  2. 怎样把脑海中的“泛型”变成真正“泛型”的代码?

举个例子:generic typed function ‘add’

在我遇到的朋友中,即便如此对他解释了模板,即便他了解了模板,也仍然会对模板产生畏难情绪。毕竟从形式上来说,模板类和模板函数都要较非模板的版本更加复杂,阅读代码所需要理解的内容也有所增多。

如何才能克服这一问题,最终视模板如平坦代码呢?

答案只有一个:无他,唯手熟尔

在学习模板的时候,要反复做以下的思考和练习:

  1. 提出问题:我的需求能不能用模板来解决?

  2. 怎么解决?

  3. 把解决方案用代码写出来。

  4. 如果失败了,找到原因。是知识有盲点(例如不知道怎么将 T& 转化成 T),还是不可行(比如试图利用浮点常量特化模板类,但实际上这样做是不可行的)?

通过重复以上的练习,应该可以对模板的语法和含义都有所掌握。如果提出问题本身有困难,或许下面这个经典案例可以作为你思考的开始:

  1. 写一个泛型的数据结构:例如,线性表,数组,链表,二叉树;

  2. 写一个可以在不同数据结构、不同的元素类型上工作的泛型函数,例如求和;

当然和“设计模式”一样,模板在实际应用中,也会有一些固定的需求和解决方案。比较常见的场景包括:泛型(最基本的用法)、通过类型获得相应的信息(型别萃取)、编译期间的计算、类型间的推导和变换(从一个类型变换成另外一个类型,比如boost::function)。这些本文在以后的章节中会陆续介绍。

1.2.2 模板函数的使用

我们先来看一个简单的函数模板,两个数相加:

template <typename T> T Add(T a, T b)
{
  return a + b;
}

函数模板的调用格式是:

函数模板名 < 模板参数列表 > ( 参数 )

例如,我们想对两个 int 求和,那么套用类的模板实例化方法,我们可以这么写:

int a = 5;
int b = 3;
int result = Add<int>(a, b);

这时我们等于拥有了一个新函数:

int Add<int>(int a, int b) { return a + b; }

这时在另外一个偏远的程序角落,你也需要求和。而此时你的参数类型是 float ,于是你写下:

Add<float>(a, b);

一切看起来都很完美。但如果你具备程序员的最佳美德——懒惰——的话,你肯定会这样想,我在调用 Add<int>(a, b) 的时候, ab 匹配的都是那个 T。编译器就应该知道那个 T 实际上是 int 呀?为什么还要我多此一举写 Add<int> 呢? 唔,我想说的是,编译器的作者也是这么想的。所以实际上你在编译器里面写下以下片段:

int a = 5;
int b = 3;
int result = Add(a, b);

编译器会心领神会地将 Add 变成 Add<int>。但是编译器不能面对模棱两可的答案。比如你这么写的话呢?

int  a = 5;
char b = 3;
int  result = Add(a, b);

第一个参数 a 告诉编译器,这个 Tint。编译器点点头说,好。但是第二个参数 b 不高兴了,告诉编译器说,你这个 T,其实是 char。 两个参数各自指导 T 的类型,编译器就不知道怎么做了。在Visual Studio 2012下,会有这样的提示:

error C2782: 'T _1_2_2::Add(T,T)' : template parameter 'T' is ambiguous

好吧,"ambiguous",这个提示再明确不过了。

不过,只要你别逼得编译器精神分裂的话,编译器其实是非常聪明的,它可以从很多的蛛丝马迹中,猜测到你真正的意图,有如下面的例子:

template <typename T> class A {};

template <typename T> T foo( A<T> v );

A<int> v;
foo(v);	// 它能准确地猜到 T 是 int.

咦,编译器居然绕过了A这个外套,猜到了 T 匹配的是 int。编译器是怎么完成这一“魔法”的,我们暂且不表,2.2节时再和盘托出。

下面轮到你的练习时间了。你试着写了很多的例子,但是其中一个你还是犯了疑惑:

float data[1024];

template <typename T> T GetValue(int i)
{
  return static_cast<T>(data[i]);
}

float a = GetValue(0);	// 出错了!
int b = GetValue(1);	// 也出错了!

为什么会出错呢?你仔细想了想,原来编译器是没办法去根据返回值推断类型的。函数调用的时候,返回值被谁接受还不知道呢。如下修改后,就一切正常了:

float a = GetValue<float>(0);
int b = GetValue<int>(1);

嗯,是不是so easy啊?嗯,你又信心满满的做了一个练习:

你要写一个模板函数叫 c_style_cast,顾名思义,执行的是C风格的转换。然后出于方便起见,你希望它能和 static_cast 这样的内置转换有同样的写法。于是你写了一个use case。

DstT dest = c_style_cast<DstT>(src);

根据调用形式你知道了,有 DstTSrcT 两个模板参数。参数只有一个, src,所以函数的形参当然是这么写了: (SrcT src)。实现也很简单, (DstT)v

我们把手上得到的信息来拼一拼,就可以编写自己的函数模板了:

template <typename SrcT, typename DstT> DstT c_style_cast(SrcT v)
{
  return (DstT)(v);
}

int v = 0;
float i = c_style_cast<float>(v);

嗯,很Easy嘛!我们F6一下…咦!这是什么意思!

error C2783: 'DstT _1_2_2::c_style_cast(SrcT)' : could not deduce template argument for 'DstT'

然后你仔细的比较了一下,然后发现 … 模板参数有两个,而参数里面能得到的只有 SrcT 一个。结合出错信息看来关键在那个 DstT 上。这个时候,你死马当活马医,把模板参数写完整了:

float i = c_style_cast<int, float>(v);

嗯,很顺利的通过了。难道C++不能支持让参数推导一部分模板参数吗?

当然是可以的。只不过在部分推导、部分指定的情况下,编译器对模板参数的顺序是有限制的:先写需要指定的模板参数,再把能推导出来的模板参数放在后面

在这个例子中,能推导出来的是 SrcT,需要指定的是 DstT。把函数模板写成下面这样就可以了:

template <typename DstT, typename SrcT> DstT c_style_cast(SrcT v)	// 模板参数 DstT 需要人肉指定,放前面。
{
  return (DstT)(v);
}

int v = 0;
float i = c_style_cast<float>(v);  // 形象地说,DstT会先把你指定的参数吃掉,剩下的就交给编译器从函数参数列表中推导啦。