《C++ primer》学习笔记之二十五:template function 的实例化

来源:互联网 发布:博客源码资源站 编辑:程序博客网 时间:2024/06/06 07:35

2.何时实例化:
  (1).遇到一个函数调用或取函数地址,如果在普通函数定义中找到了匹配的定义,那么直接用普通的函数
    如果遇到显式实例化,那么直接跳到第(3)步
   比如:
   //te.cpp
   void f(int i){ cout << "f(" << i << ")" << endl; }
   f(2);    //这个调用不会引起实例化template_function
   
   //必须是精确匹配,不需要隐含的转换。下面这个不是精确匹配
   f(2.0f);  //实例化:void f(float) .
   
   此时就不要实例化了。
   
  (2).如果在普通函数中没有找到精确匹配的,则到显式规格化函数explicit_specialization_template_function中匹配
   //声明一个f的显式规格化函数:
   //te.h
   template <typename T> void f(T t);  //声明template_function f
   template <> void f<float>(float x);  //声明一个f的显式规格化函数。这个必须在f的声明或定义之后
   template <> void f(float x);   //这样显示规格化也可以(没有<float>了)。因为参数float x已表明template parameter为float
             //类推,如果从函数参数中可以推导出template parameter的值,则可以靠后的连续的几个template parameter可以省略,让编译器自己去猜
             
   template <typename T>     //定义template_function f
   void f(T t){ cout << t << endl; };             
   
   //te.cpp
   void f(int i){ cout << "f(" << i << ")" << endl; }  //普通的函数
   template <> void f(float x)        //explicit_specializztion_template_function定义
   { cout << "f(" << x << "f)" << endl; }
   
   f(2);  //输出:f(2) 这个是普通函数
   f(2.0f); //输出:f(2f)  这个即是显式规格化的那位
   f(2.0);  //输出:double 这个是template_function实例化void f(double)后的输出
   
   与普通函数一样:显式规格化template函数要先在header中声明,然后再在.cpp中定义,否则出现“重复定义”的编译错
       不过很奇怪的是,可以在同一个.cpp中定义多次显式规格化template函数,且body各异。不过结果是以第一次的为准。
       但不可以在不同的.cpp中定义同一个显示规格化template函数,否则出现“重复定义”的编译错
   与普通函数一样:如果显示规格化template函数只声明却没有定义,则会报“定义找不到”的link错
      
   此时的匹配与1中一样,也是精确匹配
  (3).如果2也没有找到匹配的函数,则到template_function中匹配
   匹配时,找符合调用函数参数的template_function,并用匹配的类型去实例化template_function。怎么匹配,请看3如何实例化
   该实例化是隐式实例化
   //te.cpp
   class ClassA{}
   template <class T>
   class TemClassB{}
   
   int main()
   {
    f(2.0);    //输出:f(double)  ——类似的还有f(2L):f(long), f(2LU):f(unsigned long)
    f('a');    //f(char)
    f("a");    //f(char const *)  ——注意这里有个const修饰,所以在f中试图改变指针的内容会报编译错。
    char *p = "a";
    f(p);    //f(char *)
    char *&rp = p;
    f(rp);    //f(char *)    ——注意引用符号并没有输出来,但引用符号确实可以作为重载的材料
    
    ClassA a;
    f(a);    //f(class ClassA)  ——类似的还有f(union un), f(enum en)
    
    TemClassB<int> b;
    f(b);    //f(class TemClassB<int>)
    
    string str = "a";
    f(str);    //f(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >)
    
    TemClassB<string> bs;
    f(bs);    //f(class B<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >)
   
    class LocalClass{};
    LocalClass c;
    f(c);    //error.在模板实例化中使用局部类型
    
    return 0;
   }
   

 3. 如何实例化
  实质就是编译器决定用什么template_parameter去实例化模板函数。
  实际上在第2步中有个匹配的过程,"匹配"即是决定模板参数的过程。
  (1) 每个调用函数使用的参数将与函数定义的参数一一对应,但不对应返回值。
   显然,参数个数要一样。template_function的template_parameter不能有default值,所以不存在省略某个的参数的做法
   template <class T> void f(T t1, T t2) {}
   f("a", "b"); //调用参数"a"对应T t1, 调用参数"b"对应T t2       
   
   template <class T, class S> S f(T t) { return S(); }
   f("a");     //调用参数"a"对应T t
   int (*pf)(float) = f; //调用参数类型float对应T,返回值类型int不对应S
      
  (2) 如果定义中,参数的类型为已知类型,即不含template_parameter中的类型。那么调用的参数可以作ordinary_conversion常规转换到要求的类型
   如果定义中,参数的类型为parameter type,那么如果调用的参数作下面的转换可以到要求的类型,则也算匹配:
    1. lvalue -> rvalue:
      parameter type为rvalue,而调用的参数为lvalue
      比如:
       template <class T> void f(T t) {}
       template <class S> void g(S &s) {}
       int main()
       {
        f(0);  //这里就不需要lvalue-rvalue的转换,因为0为rvalue, t也为rvalue
        
        int i = 0;  
        f( i );  //这里就需要lvalue-rvalue的转换,因为i为rvalue, t为rvalue
        
        g(0);  //error:int不能转换为int&. 这里企图进行rvalue-lvalue的转换,但是不容许
        g(i);  //i为rvalue, s为rvalue,不需要任何转换
       }
      一般,如果函数的参数和返回值不是一个引用类型,那么它返回的就是一个rvalue的转换。
    2. qualification conversion:
      这里的qualification指const, volatile.
      parameter type中含这些qualification,但调用的参数中则没有
      比如:
       template <class T> void f(T const t) {}
       int main()
       {                
        int i = 0;  
        f( i );  //t为int const类型,而i为int类型。qualification conversion发生        
       }
    3. derived-base
      parameter type为基类,而调用的参数为子类
    
    注意:当参数类型为数组类型时,其实质为指针。所以数组——指针不算转换。
    
   这三种转换为exactly conversion.
   当然,如果可能,编译器不做任何转换即得出parameter type的值。
   经过本匹配,编译器可以得到一个parameter type的值
  (3)依次顺序地分析所有调用的参数。
   如果分析中发现某个parameter type有两种类型,编译器是不会试着去转换这两者类型的,直接pass掉,即认为调用与本目标不匹配。
    template <class T> void f(T t1, T t2) {}
    f('1', 2);  //error:模板参数T不明确
        //分析参数'1'时得出T为char, 分析参数2时得出T为int,两者矛盾
    
   如果分析完后,parameter list中仍然有人没有值,那么匹配不成功。
    template <class T, class S> S f(T t) { return S(); }
    int i = f(2); //error:未能推导出S的模板参数
        //T为int
        //虽然调用时要求返回值为int类型,但编译器是靠函数调用的参数列表来分析出模板参数的。此时分析不出来S的类型来。
            
    int (*pf)(int) = f;  //error:未能推导出S的模板参数。原因与上面相同
   
  (4)按(1)(2)(3)步骤匹配其它重载的函数,找到"最匹配的函数"。
   如果有两个“最匹配的函数”,则报“ambiguous call to overloaded function”编译错,即“两义调用”
   比如:
    template <class T> void f(T t1, T t2) {}  //1
    template <class T> void f(T t1, T const t2) {} //error. const不能作为重载的材料
    template <class T> void f(T t1, double d) {} //3
    template <class T> void f(char c, T t2) {}  //4
    
    template <class T> void g(T t1, double d) {} //5
    
    int main()
    {
     f(1, 3);  //1匹配, T 为 int,
     f(1, 3.0);  //3匹配, T 为 int
     f('1', 3);  //4匹配, T 为 int
     f('1', 3.0); //error: 3和4都为最佳匹配,则为两义调用。
     
     g(1, 3);  //5匹配, T 为int。 参数3会转换为类型double
     
    }
  
   例外:
    template <class T> void f(T t1, T t2) {}
    template <class T, class S> void f(T t, S s) {}
    f(3, 4)  //调用 f(T t1, T t2) 还是 f(T t, S s)?好像都是最佳匹配?是否有编译错?
       //ok. 调用f(T t1, T t2)。
       //与两函数定义的次序没有关系。具体原因未知。按理f(T t1, T t2)更匹配是合理的,难道是"最佳最简单匹配"?
       
   不过:
    template <class T, class S> S f(T t1, T t2) {}  //与上面的区别是返回值为S类型
    template <class T, class S> S f(T t, S s) {}  //与上面的区别是返回值为S类型
    f(3, 4); //ok.调用f(T t, S s)。
       //因为f(T t1, T t2)函数推导不出S的值,所以pass掉。此处是与上面的例外作比较。
   
   注意:如果让编译器自己推导template_parameter的值不太直观的话,可以显示调用,即直接指定参数的值:
    f<int, int>(3, 4);
    f<int, double>(3, 4); //这个调用参数4会转换为double型
    f<int, string>(3, 4); //error:4无法转换为std::string类型
    
    如果靠后的参数编译器自己可以推导出来,那么可以省略:
     f<int>(3, 4);
     f<int>(3, 4.0);  //这个与f<int, double>(3, 4.0)等

   上面的输出中显示了匹配的函数规格,这个也就是用来作为实例化模板函数的template_parameter
    
   如果第3步也没有找到匹配的函数,则该调用就错了。
   
  一旦在第(2)、(3)步中引起了实例化,那么实例化生成的函数就成为了一个普通的函数,那么它就会碰到些普通函数会遇到的问题:
   1.如果在实例化地方的后面又有相同原形的函数定义,则报“重复定义”
    比如在两个.cpp中都显式规格化同一个template_function
    
    比如在实例化地方的后面又有一个普通函数的定义或显示规格化template_function的定义有相同原形
        
    在header中声明所有函数,或在调用前声明所有函数,可避免这种错误。
    
   2.实例化后所有template parameter都确定了,所以在函数体中使用这些parameter的地方是否符合语法,编译器会检查
    比如:
     template <class T>
     void f(T t) { cout << T << T.size() << endl; }
    函数f假定类型T支持'<<'操作符,并且有size()成员函数,如果实例化后发现没有,则会报编译错。
    这种错误正是显式规格化模板函数的最大用处。对于例外的且常见的类型显式定义一个规格可以解决本问题。
   3. 同一个文件中,如果再碰到匹配的函数调用,则编译器不会再实例化了。  
      但在不同的文件中,会各自实例化同一个函数规格,好像它们不去判断其他人是否已实例化同一个函数规格。