跳转到内容

模板 (C++)

本页使用了标题或全文手工转换
维基百科,自由的百科全书

这是本页的一个历史版本,由唐僧下凡留言 | 贡献2015年1月22日 (四) 09:23 template关键字:​ 因为原来的代码编译不能通过,以上修改可以在linux下正常编译)编辑。这可能和当前版本存在着巨大的差异。

模板Template)指C++程序设计语言中的函数模板与模板[1],是一种参数化类型机制,大体对应于javaC#中的泛型,但也有一些功能上的显著差异(C++模板支持后两者没有明确对应的模板模板参数和模板非类型参数,但不支持Java的通配符以及C#的泛型类型约束)。模板是C++的泛型编程中不可缺少的一部分。

模板定义以关键字template开始,后接模板形参表(template parameter list),模板形参表是用尖括号括住的一个或者多个模板形参的列表,形参之间以逗号分隔。模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。非类型形参跟在类型说明符之后声明。类型形参跟在关键字class或typename之后声明。

使用模板时,可以在模板名字后面显式给出用尖括号括住的模板实参列表(template argument list)。对模板函数或类的模板成员函数,也可不显式给出模板实参,而是由编译器根据函数调用的上下文推导出模板实参,这称为模板参数推导

模板是C++程序员绝佳的武器,特別是結合了多重继承运算符重载之后。C++的标准函数库提供的许多有用的函数大多結合了模板的概念,如STL以及iostream

簡介

函数模板

以下以取最大值的函数模板maximum为例。此函数在编译时会自动产生对应参数类型的代码,而不用显式声明。

#include <iostream>

template <typename T>
inline const T& maximum(const T& x,const T& y)
{
   if(y > x){
      return y;
   }
   else{
      return x;
   }
}

int main(void)
{
   using namespace std;
   //Calling template function
   std::cout << maximum<int>(3,7) << std::endl;         //输出 7
   std::cout << maximum(3, 7) << std::endl;             //自动补充类型声明
   std::cout << maximum<double>(3.0,7.0) << std::endl;  //输出 7
   return 0;
}

类模板

以下以將元件指標的操作,封裝成类別模板ComPtr為例。
#pragma once

template <typename Ty>
class ComPtr
{
protected:
    Ty* m_ptr;
    
public:

    ComPtr()
    {
        m_ptr = NULL;
    }

    ComPtr(const ComPtr& rhs)
    {
        m_ptr = NULL;
        SetComPtr(rhs.m_ptr);
    }

    ComPtr(Ty* p)
    {
        m_ptr = NULL;
        SetComPtr(p);
    }

    ~ComPtr()
    {
        Release();
    }

    const ComPtr& operator=(const ComPtr& rhs)
    {
        SetComPtr(rhs.m_ptr);
        return *this;
    }

    Ty* operator=(Ty* p)
    {
        SetComPtr(p);
        return p;
    }

    operator Ty* ()
    {
        return m_ptr;
    }

    Ty* operator->()
    {
        return m_ptr;
    }

    operator Ty** ()
    {
        Release();
        return &m_ptr;
    }

    operator void** ()
    {
        Release();
        return (void**)&m_ptr;
    }

    bool IsEmpty()
    {
        return (m_ptr == NULL);
    }

    void SetComPtr(Ty* p)
    {
        Release();
        
        m_ptr = p;
        if (m_ptr)
        {
            m_ptr->AddRef();
        }
    }

    void Release()
    {
        if (m_ptr)
        {
            m_ptr->Release();
            m_ptr = NULL;
        }
    }
};

模板的嵌套:成员模板

对于类中的模板成员函数、嵌套的成员类模板, 可以在封闭类的内部或外部定义它们。当模板成员函数、嵌套类模板在其封闭类的外部定义时,必须以封闭类模板的模板参数(如果它们也是模板类)和成员模板的模板参数开头。[1]如下例:

template <typename C> class myc{
  public:
    template <typename S> C foo(S s);
};

//下行需要给出外部类与内部嵌套类的模板形参列表:
template<typename C> template <typename S> C myc<C>::foo(S s){
C var;
return var;   
}

int main()
{
float f;
myc<int> v1;
v1.foo(f);
}

C++标准规定:如果外围的类模板没有特例化,里面的成员模板就不能特例化[2]。例如:

template <class T1> class A {
  template<class T2> class B {
      template<class T3> void mf1(T3);
      void mf2();
  };
};

template <> template <class X>
   class A<int>::B {
      template <class T> void mf1(T);
   };

template <> template <> template<class T>
    void A<int>::B<double>::mf1(T t) { }

template <class Y> template <>
     void A<Y>::B<double>::mf2() { } // ill-formed; B<double> is specialized but its enclosing class template A is not

依赖名字与typename关键字

一个模板中的依赖于一个模板参数(template parameter)的名字被称为依赖名字 (dependent name)。当一个依赖名字嵌套在一个类的内部时,称为嵌套依赖名字(nested dependent name)。一个不依赖于任何模板参数的名字,称为非依赖名字(non-dependent name)。[3]

编译器在处理模板定义时,可能并不确定依赖名字表示一个类型,还是嵌套类的成员,还是类的静态成员。C++标准规定:如果解析器在一个模板中遇到一个嵌套依赖名字,它假定那个名字不是一个类型,除非显式用typename关键字前置修饰该名字。[4]

typename关键字有两个用途:

  1. 常见的在模板定义中的模板形参列表,表示一个模板参数是类型参数。等同于使用class
  2. 使用模板类内定义的嵌套依赖类型名字时,显式指明这个名字是一个类型名。否则,这个名字会被理解为模板类的静态成员名。

在下述情形,对嵌套依赖类型名字不需要前置修饰typename关键字:[5]

  • 派生类声明的基类列表中的基类标识符;
  • 成员初始化列表中的基类标识符;
  • classstructenum等关键字开始的类型标识符

因为它们的上下文已经指出这些标识符就是作为类型的名字。例如:

template <class T> class A: public T::Nested { //基类列表中的T::Nested
  public:
    A(int x) : T::Nested(x) {}; //成员初始化列表中的T::Nested
    struct T::type1 m; //已经有了struct关键字的T::type1
};

class B{
  public:
    class Nested{
      public:
           Nested(int x){};
    };
    typedef struct {int x;} type1;
};

int main()
{
  A<B> a(101);
  return 0;
}

template关键字

template关键字有两个用途:

  1. 常见的在模板定义的开始。
  2. 是模板类内部定义了模板成员函数或者嵌套的成员模板类。当引用这样的模板成员函数或嵌套的成员模板类时,可以在::(作用域解析)运算符、.(以对象方式访问成员)运算符、->(以指针方式访问成员)运算符之后是模板成员函数名字或嵌套的成员模板类名字,随后的左尖括号<将被解释为模板参数列表的开始。例如:
#include <iostream>
using namespace std;

template <class T>
class A{
    template <class U>
    class B{
    	public: 
	   typedef int INT;
    };
    public:
	template <class V> 
	void foo(){};
};

int main()
{
  A<float>::B<double>::INT i;
  i=101;
  A<bool> a, *p=&a;
  a.foo<char>();
  p->foo<long>();
  return 0;
}

模板實例化

模板实例化(template instantiation )是指在编译或链接时生成函数模板或类模板的具体实例源代码。ISO C++定义了两种模板实例化方法:隐式实例化(当使用实例化的模板时自动地在当前代码单元之前插入模板的实例化代码)、显式实例化(直接声明模板实例化)。在C++语言的不同实现中,模板编译模式(模板初始化的方法)大致可分为三种:

  • Borland模型(包含模板编译模式):编译器生成每个编译单元中遇到的所有的模板实例,并存放在相应的目标文件中;链接器合并相同的模板实例,生成可执行文件。为了在每次模板实例化时模板的定义都是可见的,模板的声明与定义放在同一个.h文件中。这种方法的优点是链接器只需要处理目标文件;这种方法的缺点是由于模板实例被重复编译,编译时间被加长了,而且不能使用系统的链接器,需重新设计链接器
  • Cfront/查询模型(分离(Separation)模板编译模式):AT&T公司C++编译器Cfront为解决模板实例化问题,增加了一个模板仓库,用以存放模板实例的代码并可被自动维护。当生成一个目标文件时,编译器把遇到的模板定义与当前可生成的模板实例存放到模板仓库中。链接时,链接器的包装程序(wrapper)首先调用编译器生成所有需要的且不在模板仓库中的模板实例。这种方法的优点是编译速度得到了优化,而且可以直接使用系统的链接器;这种方法的缺点是复杂度大大增加,更容易出错。使用这种模型的源程序通常把模板声明与非内联的模板成员分别放在.h文件与模板定义文件中,后者单独编译
  • 混合(迭代)模型:g++目前是基于Borland模型完成模板实例化。g++未来将实现混合模型的模板实例化,即编译器编译单元中的模板定义与遇到的当前可实现的模板实例存放在相应的目标文件中;链接器的包装程序(wrapper)调用编译器生成所需的目前还没有实例化的模板实例;链接器合并所有相同的模板实例。使用这种模型的源程序通常把模板声明与非内联的模板成员分别放在.h文件与模板定义文件中,后者单独编译

ISO C++标准规定,如果隐式实例化模板,则模板的成员函数一直到引用时才被实例化;如果显式实例化模板,则模板所有成员立即都被实例化,所以模板的声明与定义在此处都应该是可见的,而且在其它程序文本文件使用了这个模板实例时用编译器选项抑制模板隐式实例化,或者模板的定义部分是不可见的,或者使用template<> type FUN_NAME(type list)的语句声明模板的特化但不实例化。

g++的模板实例化,目前分为三种方式:[6]

  • 不指定任何特殊的编译器参数:按Borland模型写的源代码能正常完成模板实例化,但每个编译单元将包含所有它用到的模板实例,导致在大的程序中无法接受的代码冗余。需要用GNU链接器删除各个目标文件中冗余的模板实例,不能使用操作系统提供的链接器
  • 使用-fno-implicit-templates编译选项:在生成目标文件时完全禁止隐式的模板实例化,所有模板实例都显式的写出来,可以存放在一个单独的源文件中;也可以存放在各个模板定义文件中。如果一个很大的源文件中使用了各个模板实例,这个源文件不用-fno-implicit-templates选项编译,就可以自动隐式的生成所需要的模板实例。在生成库文件时这个编译选项特别有用。
  • 使用-frepo编译选项:在生成每个目标文件时,把需要用到的当前可生成的模板实例存放在相应的.rpo文件中。链接器包装程序(wrapper)—collect2将删除.rpo文件中冗余的模板实例并且修改相应的.rpo文件,使得编译器可以利用.rpo文件知道在那里正确放置、引用模板实例,并重新编译生成受影响的目标文件。由操作系统的通用的链接器生成可执行文件。这对Borland模型是很好的模板实例化方法。对于使用Cfront模型的软件,需要修改源代码,在模板头文件的末尾加上#include <tmethods.cc>。不过MinGW中不包含链接器包装程序collect2,故不使用此方法。对于库(library),建议使用显式实例化方法。
  • 另外,g++扩展了ISO C++标准,用extern关键字指出模板实例在其它编译单元中显式声明(这已经被C++11标准接受);用inline关键字实例化编译器支持的数据(如虚表)但不实例化模板成员;用static关键字实例化模板的静态数据成员但不实例化其它非静态的模板成员。
  • g++不支持模板实例化的export关键字(此关键字的这个用法已在C++11标准里被取消)。

VC++7.0中必须类模板实例化只有Borland模型;函数模板一般隐式实例化,自5.0版以后也可显式实例化。

参考文献

  1. ^ 1.0 1.1 MSDN:嵌套的类模板
  2. ^ C++11标准:§14.7.3,¶16规定:the declaration shall not explicitly specialize a class member template if its enclosing class templates are not explicitly specialized as well
  3. ^ C++11标准:§14.6,¶1
  4. ^ C++11标准§14.6,¶2规定:A name used in a template declaration or definition and that is dependent on a template-parameter is assumed not to name a type unless the applicable name lookup finds a type name or the name is qualified by the keyword typename.
  5. ^ C++11标准§14.6,¶5规定
  6. ^ Template Instantiation. [2014-09-27].