函数模板使用

初探函数模板

函数模板提供了一种函数行为,该函数行为可以用多种不同的类型进行调用;也就是说,函数模板代表一个函数家族。它的表示(即外形)看起来和普通的函数很相似,唯一的区别就是有些函数元素是未确定的:这些元素将在使用时被参数化。为了阐明这些概念,让我们先来看一个简单的例子。

定义模板

下面就是一个返回两个值中最大者的函数模板:

    //basics/max.hpp
    template <typename T>
    inline T const& max (T const& a, T const& b)
    {
        // 如果a < b,那么返回b,否则返回a
        return  a < b ? b : a;
    }

这个模板定义指定了一个“返回两个值中最大者”的函数家族,这两个值是通过函数参数a和b传递给该函数模板的;而参数的类型还没确定,用模板参数T来代替。如例子中所示,模板参数必须用如下形式的语法来声明:

    template < comma-separated-list-of-parameters >
   //template < 用逗号隔开的参数列表 >

在我们这个例子里,参数列表是typename T。可以看到:我们用小于号和大于号来组成参数列表外部的一对括号,并把它们称作尖括号。关键字typename引入了所谓的类型参数T,到目前为止它是C++程序使用最广泛的模板参数;也可以用其他的一些模板参数

在上面程序中,类型参数是T。你可以使用任何标识符作为类型参数的名称,但使用T已经成为了一种惯例。事实上,类型参数T表示的是,调用者调用这个函数时所指定的任意类型。你可以使用任何类型(基本类型、类等)来实例化该类型参数,只要所用类型提供模板使用的操作就可以。例如,在这里的例子中,类型T需要支持operator<,因为a和b就是使用这个运算符来比较大小的。

鉴于历史的原因,你可能还会使用class取代typename,来定义类型参数。在C++语言的演化过程中,关键字typename的出现相对较晚一些;在它之前,关键字class是引入类型参数的唯一方式,并一直作为有效方式保留下来。因此,模板max()还可以有如下的等价定义:

    template <class T>
    inline T const& max (T const& a, T const& b)
    {
        // 如果a < b,那么返回b,否则返回a
        return a < b ? b : a;
    }

从语义上讲,这里的class和typename是等价的。因此,即使在这里使用了class,你也可以用任何类型(前提是该类型提供模板使用的操作)来实例化模板参数。然而,class的这种用法往往会给人误导(这里的class并不意味着只有类才能被用来替代T,事实上基本类型也可以);因此对于引入类型参数的这种用法,你应该尽量使用typename。另外还应该注意,这种用法和类的类型声明不同,也就是说,在声明(引入)类型参数的时候,不能用关键字struct代替typename。

使用模板

下面的程序展示了如何使用max()函数模板:

    //basics/max.cpp
    #include <iostream>
    #include <string>
    #include ”max.hpp”
        int main()
        {
       int i = 42;
        std::cout << “max(7, i) :  “ << ::max(7, i) <<std::endl;

        double f1 = 3.4;
        double f2 = -6.7;
        std::cout << “max(f1, f2):  “ << ::max(f1, f2) <<std::endl;

        std::string s1 = “mathematics”;
        std::string s2 = “math”;
        std::cout << “max(s1, s2):  “ << ::max(s1, s2) <<std::endl;
    }

在上面的程序里,max()被调用了3次,调用实参每次都不相同:一次用两个int,一个用两个double,一次用两个std::string。每一次都计算出两个实参的最大值,而调用结果是产生如下的程序输出:

    max(7, i):42
    max(f1, f2):3.4
    max(s1, s2):mathematics

可以看到:max()模板每次调用的前面都有域限定符::,这是为了确认我们调用的是全局名字空间中的max()。因为标准库也有一个std::max()模板,在某些情况下也可以被使用,因此有时还会产生二义性。通常而言,并不是把模板编译成一个可以处理任何类型的单一实体;而是对于实例化模板参数的每种类型,都从模板产生出一个不同的实体。因此,针对3种类型中的每一种,max()都被编译了一次。例如,max()的第一次调用:

    int i = 42;
    … max(7, i) …

使用了以int作为模板参数T的函数模板。因此,它具有调用如下代码的语义:

    inline int const& max (int const& a, int const& b)
    {
        // 如果a < b,那么返回b,否则返回a
        return  a < b ? b : a;
    }

这种用具体类型代替模板参数的过程叫做实例化(instantiation)。它产生了一个模板的实例。遗憾的是,在面向对象的程序设计中,实例和实例化这两个概念通常会被用于不同的场合——但都是针对一个类的具体对象。然而,由于本书叙述的是关于模板的内容,所以在未做特别指定的情况下,我们所说的实例指的是模板的实例。

可以看到:只要使用函数模板,(编译器)会自动地引发这样一个实例化过程,因此程序员并不需要额外地请求模板的实例化。类似地,max()的其他调用也将为double和std::string实例化max模板,就像具有如下单独的声明和实现一样:

    const double& max (double const&, double const&);
    const std::string& max ( std::string const&,
                                std::string const&);

如果试图基于一个不支持模板内部所使用操作的类型实例化一个模板,那么将会导致一个编译期错误,例如:

    std::complex<float>  c1, c2;  //std::complex并不支持operator <
    …
    max(c1, c2);                   //编译器错误

于是,我们可以得出一个结论:模板被编译了两次,分别发生在

1. 实例化之前,先检查模板代码本身,查看语法是否正确;在这里会发现错误的语法,如遗漏分号等。

2. 在实例化期间,检查模板代码,查看是否所有的调用都有效。在这里会发现无效的调用,如该实例化类型不支持某些函数调用等。

这给实际中的模板处理带来了一个很重要的问题:当使用函数模板,并且引发模板实例化的时候,编译器(在某时刻)需要查看模板的定义。这就不同于普通函数中编译和链接之间的区别,因为对于普通函数而言,只要有该函数的声明(即不需要定义),就可以顺利通过编译。

实参的演绎

当我们为某些实参调用一个诸如max()的模板时,模板参数可以由我们所传递的实参来决定。如果我们传递了两个int给参数类型T const&,那么C++编译器能够得出结论:T必须是int。注意,这里不允许进行自动类型转换;每个T都必须正确地匹配。例如:

    template <typename T>
    inline T const& max (T const& a, T const& b);
    …
    max(4,7)        //OK: 两个实参的类型都是int
    max(4,4.2)      //ERROR:第1个T是int,而第2个T是double

有3种方法可以用来处理上面这个错误:

  • 对实参进行强制类型转换,使它们可以互相匹配:
   max ( static_cast<double>(4),4.2)       //OK
  • 显式指定(或者限定)T的类型:
    max<double>(4,4.2)                       //OK
  • 指定两个参数可以具有不同的类型。

模板参数

函数模板有两种类型的参数。

1. 模板参数:位于函数模板名称的前面,在一对尖括号内部进行声明:

   template <typename T>             //T是模板参数

2. 调用参数:位于函数模板名称之后,在一对圆括号内部进行声明:

   max (T const& a, T const& b)     //a和b都是调用参数

你可以根据需要声明任意数量的模板参数。然而,在函数模板内部(这一点和类模板有区别),不能指定缺省的模板实参。例如,你可以定义一个“两个调用参数的类型可以不同的”max()模板:

    template <typename T1, typename T2>
    inline T1 max (T1 const& a, T2 const& b)
    {
        return  a < b ? b: a;
    }
    …
    max(4,4.2)     //OK, 但第1个模板实参的类型定义了返回类型

这看起来是一种能够给max()模板传递两个不同类型调用参数的好方法,但在这个例子中,这种方法是有缺点的。主要问题是:必须声明返回类型。对于返回类型,如果你使用的是其中的一个参数类型,那么另一个参数的实参就可能要转型为返回类型,而不会在意调用者的意图。C++并没有提供一种“指定并且选择一个‘最强大类型’”的途径(然而,你可以使用一些tricky模板编程来提供这个特性,详见15.2.4小节)。于是,取决于调用实参的顺序,42和66.66的最大值可以是浮点数66.66,也可以是整数66。另一个缺点是:把第2个参数转型为返回类型的过程将会创建一个新的局部临时对象,这导致了你不能通过引用来返回结果。因此,在我们的例子里,返回类型必须是T1,而不能是T1 const&。

因为调用参数的类型构造自模板参数,所以模板参数和调用参数通常是相关的。我们把这个概念称为:函数模板的实参演绎。它让你可以像调用普通函数那样调用函数模板。

然而,如前所述,针对某些特定的类型,你还可以显式地实例化该模板:

    template <typename T>
    inline T const& max (T const& a, T const& b);
    …
    max<double>(4,4.2)   //用double来实例化T

当模板参数和调用参数没有发生关联,或者不能由调用参数来决定模板参数的时候,你在调用时就必须显式指定模板实参。例如,你可以引入第3个模板实参类型,来定义函数模板的返回类型:

    template <typename T1, typename T2, typename RT>
    inline RT max (T1 const& a, T2 const& b);

然而,模板实参演绎并不适合返回类型,因为RT不会出现在函数调用参数的类型里面。因此,函数调用并不能演绎出RT。于是,你必须显式地指定模板实参列表。例如:

    template <typename T1, typename T2, typename RT>
    inline RT max (T1 const& a, T2 const& b);
    …
    max<int, double, double>(4,4.2)   //OK, 但是很麻烦

到目前为止,我们只是考察了显式指定所有函数模板实参的例子,和不显式指定函数任何模板实参的例子。另一种情况是只显式指定第一个实参,而让演绎过程推导出其余的实参。通常而言,你必须指定“最后一个不能被隐式演绎的模板实参之前的”所有实参类型。因此,在上面的例子里,如果你改变模板参数的声明顺序,那么调用者就只需要指定返回类型:

    template <typename RT, typename T1, typename T2>
    inline RT max (T1 const& a, T2 const& b);
    …
    max<double>(4,4.2)   //OK: 返回类型是double

在这个例子里,调用max时显式地把RT指定为double,但其他两个参数T1和T2可以根据调用实参分别演绎为int和double。可以看出,所有这些修改后的max()版本都不能得到很大的改进。由于在单(模板)参数版本里,如果传递进来的是两个不同类型的实参,你已经可以指定参数的类型(和返回类型)。因此,尽量保持简洁并且使用单参数版本的max()就是一个不错的主意(在接下来的几节里,当讨论其他模板话题的时候,我们将使用这种方法)。

重载函数模板

和普通函数一样,函数模板也可以被重载。就是说,相同的函数名称可以具有不同的函数定义;于是,当使用函数名称进行函数调用的时候,C++编译器必须决定究竟要调用哪个候选函数。即使在不考虑模板的情况下,做出该决定的规则也已经是相当复杂,但在这一节里,我们将讨论有关模板的重载问题。如果你对不含模板的重载的基本规则还不是很熟悉,那么请先阅读附录B,在那里我们对重载解析规则进行了很详细的叙述。下面的简短程序叙述了如何重载一个函数模板:

    //basics/max2.cpp
    //求两个int值的最大值
    inline int const& max (int const& a, int const& b)
    {
        return  a < b ? b : a;
    }

    // 求两个任意类型值中的最大者
    template <typename T>
    inline T const& max (T const& a, T const& b)
    {
        return  a < b ? b : a;
    }

    // 求3个任意类型值中的最大者
    template <typename T>
    inline T const& max (T const& a, T const& b, T const& c)
    {
       return ::max (::max(a, b), c);
    }

    int main()
    {
    ::max(7, 42, 68);        // 调用具有3个参数的模板
        ::max(7.0, 42.0);     // 调用max<double> (通过实参演绎)
        ::max('a', 'b');      // 调用max<char> (通过实参演绎)
        ::max(7, 42);         // 调用int重载的非模板函数
        ::max<>(7, 42);       // 调用max<int> (通过实参演绎)
        ::max<double>(7, 42); //调用max<double> (没有实参演绎)
        ::max('a', 42.7);     //调用int重载的非模板函数
    }

如例子所示,一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。对于非模板函数和同名的函数模板,如果其他条件都是相同的话,那么在调用的时候,重载解析过程通常会调用非模板函数,而不会从该模板产生出一个实例。第4个调用就符合这个规则:

    max(7,42)                //使用两个int值,很好地匹配非模板函数

然而,如果模板可以产生一个具有更好匹配的函数,那么将选择模板。这可以通过max()的第2次和第3次调用来说明:

    max(7.0,42.0);           //调用max<double>(通过实参演绎)
    max('a', 'b');           //调用max<char>(通过实参演绎)

还可以显式地指定一个空的模板实参列表,这个语法好像是告诉编译器:只有模板才能来匹配这个调用,而且所有的模板参数都应该根据调用实参演绎出来:

    max<>(7,42)               //call max<int>(通过实参演绎)

因为模板是不允许自动类型转化的;但普通函数可以进行自动类型转换,所以最后一个调用将使用非模板函数(‘a’和42.7都被转化为int):

    max('a',42.7)            //对于不同类型的参数,只允许使用非模板函数

下面这个更有用的例子将会为指针和普通的C字符串重载这个求最大值的模板:

    //basics/max3.cpp
    #include <iostream>
    #include <cstring>
    #include <string>

    // 求两个任意类型值的最大者
    template <typename T>
    inline T const& max (T const& a, T const& b)
   {
        return  a < b  ?  b : a;
    }

    // 求两个指针所指向值的最大者
    template <typename T>
    inline T* const& max (T* const& a, T* const& b)
    {
        return  *a < *b  ?  b : a;
    }

    // 求两个C字符串的最大者
    inline char const* const& max (char const* const& a,
                              char const* const& b)
    {
        return  std::strcmp(a, b) < 0  ?  b : a;
    }

    int main ()
    {
    int a=7;
        int b=42;
        ::max(a, b);     // max() 求两个int值的最大值
        std::string s="hey";
        std::string t="you";
        ::max(s, t);    // max() 求两个std:string类型的最大值

        int* p1 = &b;
        int* p2 = &a;
        ::max(p1, p2);   // max() 求两个指针所指向值的最大者

        char const* s1 = "David";
        char const* s2 = "Nico";
        ::max(s1, s2);   // max() 求两个c字符串的最大值
    }

注意,在所有重载的实现里面,我们都是通过引用来传递每个实参的。一般而言,在重载函数模板的时候,最好只是改变那些需要改变的内容;就是说,你应该把你的改变限制在下面两种情况:改变参数的数目或者显式地指定模板参数。否则就可能会出现非预期的结果。例如,对于原来使用传引用的max()模板,你用C-string类型进行重载;但对于现在(即重载版本的)基于C-strings的max()函数,你是通过传值来传递参数;那么你就不能使用3个参数的max()版本,来对3个C-string求最大值:

    //basics/max3a.cpp
   #include <iostream>
    #include <cstring>
    #include <string>

    // 两个任意类型值的最大者 (通过传引用进行调用)
    template <typename T>
    inline T const& max (T const& a, T const& b)
    {
        return  a < b  ?  b : a;
    }

    // 两个C字符串的最大者 (通过传值进行调用)
    inline char const* max (char const* a, char const* b)
    {
        return  std::strcmp(a, b) < 0  ?  b : a;
    }

    // 求3个任意类型值的最大者 (通过传引用进行调用)
    template <typename T>
    inline T const& max (T const& a, T const& b, T const& c)
    {
        return max (max(a, b), c);  //注意:如果max(a, b)使用传值调用
                                    //那么将会发生错误
    }

    int main ()
    {
        ::max(7, 42, 68);    // OK

        const char* s1 = "frederic";
        const char* s2 = "anica";
        const char* s3 = "lucas";
        ::max(s1, s2, s3);   // 错误。

    }

问题在于:如果你对3个C-strings调用max(),那么语句:

    return max (max(a, b), c);

将会产生一个错误。这是因为对于C-strings而言,这里的max(a, b)产生了一个新的临时局部值,该值有可能会被外面的max函数以传引用的方式返回,而这将导致传回无效的引用。对于复杂的重载解析规则所产生的结果,这只是具有非预期行为的代码例子中的一例而已。例如,当调用重载函数的时候,调用结果就有可能与该重载函数在此时可见与否这个事实有关,但也可能没有关系。事实上,定义一个具有3个参数的max()版本,而且直到该定义处还没有看到一个具有两个int参数的重载max()版本的声明;那么这个具有3个int实参的max()调用将会使用具有2个参数的模板,而不会使用基于int的重载版本max():

    //basics/max4.cpp
    // 求两个任意类型值的最大者
    template <typename T>
    inline T const& max (T const& a, T const& b)
    {
        return  a < b ? b : a;
    }

    // 求3个任意类型值的最大者
    template <typename T>
    inline T const& max (T const& a, T const& b, T const& c)
    {
        return max (max(a, b), c);     //使用了模板的版本,即使有下面声明的int
                                      //版本,但该声明来得太迟了
    }
    // 求两个int值的最大者
    inline int const& max (int const& a, int const& b)
    {
        return  a < b ? b : a;
    }

你应该牢记一条首要规则:函数的所有重载版本的声明都应该位于该函数被调用的位置之前。

举报
评论 0