当前位置: 首页 > 工具软件 > List Template > 使用案例 >

C++ 基础技术再深入(模板)template parameter和template argument(10)---《C++ Templates》

钱飞翼
2023-12-01

参数化声明

template和class或者function的区别在于templates声明语句有一个参数化子句:
template <…parameters here…>
或者:
export template <…parameters here>
如下展示两种templates:一种在class之内,即member templates,另一种在class之外且namespace scope之内(global scope也被当成一种namespace scope):

template <typename T>
class List{//namespace scope class template
public:
    template <typename T2>
    List(List<T2> const&);//member function template
    ...
};

template <typename T>
template <typename T2>
List<T>::List(List<T2> const& b){
    ...
}
template <typename T>
int Length(List<T> const&);//namespace scope function template

class Collection{
    template<typename T>
    class Node{//member class template
        ...
    };

    template <typename T>
    class Handle;//member class template,无定义

    template <typename T>
    T* alloc(){//member function template,隐寓为inline函数
        ...
    }
    ...
};

template <typename T>
class Collection::Handle{//member class template
    ...
};

定义域class外的member templates可有多重template <<…>…>参数化子句,其中一个代表template本身,其余各个子句代表外围的每一层class template。这些子句必须从最外层的class templates开始写起。

function template可以有预设的调用自变量,和一般function一样:

template <typename T>
void report_top(Stack<T> const&,int number=10);

template <typename T>
void fill(Array<T>*,T const&=T());//若T为内建类型,T()为0或者false

当fill()被调用时,如果调用者提供了第二自变量值,预设自变量便不会被实例化,这可确保如果预设自变量无法别某个特定类型T实例化的时候,不会引发编译错误。举例如下:

class value{
public:
    Value(int);
};
void init(Array<Value>* array){
    Value zero(0);
    fill(array,zero);//OK
    fill(array);//ERROR:Value没有default构造函数,所以调用失败
}

除了两种template基本类型,另有三种声明也可以被参数化,三者均相当于class template的成员定义:
1)class templates的成员函数定义;
2)class templates的nested class members(嵌套类别成员)定义;
3)class templates的static成员变量定义。
虽然它们也可以被参数化,但是它们并不是第一级templates。它们的参数完全由它们所隶属的template决定。示例如下:

template <int I>
class CupBoard{
    void open();
    class Shelf;
    static double total_weight;
    ...
}

template <int I>
void CupBoard<T>::open(){
    ...
}
template <int I>
class CupBoard<I>::Shelf{
    ...
};
template <int I>
double CupBoard<I>::total_weight=0.0;
  • 虚拟成员函数

member function templates不能被声明为virtual,这个限制的原因在于:虚拟函数调用机制使用一个大小固定的表格,其中每一笔条目记录一个虚拟函数入口,然而直到整个程序编译完成后才能知道有多少个member function templates需要被实例化,因此和虚拟函数调用机制冲突。
但是class template members却可以是virtual函数,因为class被实例化时候,member function的数量早就确定了,因此可以为虚拟函数。

template <typename T>
class Dynamic{
public:
    //class template的member function,可以被声明为virtual
    virtual ~Dynamic();
    //member function template,不可以被声明为virtual
    template <typename T2>
    virtual void copy(T2 const&);
};
  • template的命名机制

每个template在其作用域内必须有一个独一无二的名称,除非是被重载的function templates。需要特别注意的是class template不能喝其他不同种类的物体共享同一个名称,这点与一般的non-template class不同。

int C;
class C;//class名称和nonclass名称处在不同的空间内

int X;
template <typename T>
class X;//ERROR:名称与上述变量X冲突

struct S;
template <typename T>
class S;//ERROR:名称与上述struct S冲突

template通常使用外部链接,但不能使用C链接方式,惟一例外是static namespace scope function templates,函数内部不能再声明template,默认为:

extern "C++" tempalte <typename T>
void normal();

还有一种非标准形式的链接

extern "Xroma" template <typename T>
void Xroma_link();
tempalte <typename T>
void external();//直射另一个文件中同名且作用域相同的物体

tempalte <typename T>
static void internal();//与另一个文件中的同名template无关
  • Primary Template(主模板/原始模板)

主模板的声明语句在template名称之后并不添加由角括号括起来的template argument list

template <typename T> class Box;//OK:primary template
template <typename T> class Box<T>;//error:non-primary template
template <typename T> void translate(T*);//ok:primary template
template <tyepname T> void translate<T>(T*);//error:non-primary template

一旦我们声明一个偏特化的template,就产生了一个non-primary template。

Template Parameter(模板参数)

template parameters有三种类型:
1)Type parameters(类型参数):
2)Nontype parameters(非类型参数):
3)template template parameters(双重模板参数)。
template parameters是在template声明语句的参数化子句中生命的变量,template parameters不一定得具名:

template <typename,int>
class X;

但是当template程序代码中需要用到某个template parameter时,后者必须具名,注意:后面声明的template parameters可以用到前面声明的template parameters的名称:

template <typename T,T* Root,template <T*> class Buf>
class Structure;

下面我们各个击破。

  • Type Parameters(类别参数)

类别参数由关键字typename或者class导入,两者完全等价,声明方式是:关键词typename或class后面跟一个简单的标识符,该符号后面可以跟一个逗号以便区隔下一参数,也可以使用一个右角括号结束子句,或者跟一个等号表示预设模板自变量。
template声明语句中type parameter的作用非常类似typedef的名称。例如,你不能使用class T这样的名称,及时T确实表示一个class。

template <typename Allocator>
class List{
    class Allocator* allocator;//ERROR
    friend class Allocator;//ERROR
    ...
};
  • Nontype Parameters(非类型参数)

非类型参数实质可以在编译期或者链接期就可以确定其值的常数。这种参数的类型必须是如下三者之一:
整数(int)或者enum类型;
pointers:指向常规objects、执行functions和指向members;
reference:指向objects和指向functions。
你可能惊喜的发现,nontype parameter前面也可以有typename,示例如下:

template <typename T,typename T::Allocator* Allocator>
class List;

Nontype parameters也可以是function类型或者array类型,但它们都会退化为对应的pointer类型:

template<int buf[5]>
class Lexer;
tempalte <int* buf>
class Lexer;

Nontype template parameters的声明防护四非常类似于变量声明,但你不能加上诸如static、mutable之类的修饰,但是却可以加上const或者volatile,但如果这些修饰词出现在参数类型的最外层,编译器会忽略它们:

template <int const length> //const被忽略
class Buffer;
template <int length>
class Buffer;//与上一行声明语句等价

最后,强调一下,non type parameters总是右值:不能被取址,也不能被赋值。

Template Template Parameters(双重模板参数)
Template Template Parameter是一种class template占位符号,其声明方式和class templates类似,只是不能够使用关键词struct和union。

template <template<typename X> class C>//OK
void f(C<int>* p);

template <template<typename X> struct C>//ERROR:不能使用关键词struct
void f(C<int>* p);

template <template<typename X> union C>//ERROR:不能使用关键词 union
void f(C<int>* p);

在它们的作用域内,你可以像使用class template那样地使用template template parameters。
Template template paramters的参数也可以有default template arguments(预设模板自变量)。如果客户端没有为相应的参数指定自变量,编译器就会使用这些预设自变量:

template <template <typename T,typename A=MyAllocator> class Container>
class Adaptation{
    Container<int> storage;//等价于Container<int,MyAllocator>
    ...
};

在template template parameters中,template parameter的名称只能被用于template template parameter的其他参数声明中,示例如下:

template <template<typename T,T*> class Buf>
class Lexer{
    static char storage[5];
    Buf<char,&lexer<Buf>::storage[0]> buf;
    ...
};
template <template<typename T> class List>
class Node{
    static T* storage;//ERROR:这里不能使用template template parameters的参数T
    ...
};

为了防止上面的问题出现,通常template template parameter中template parameters名称并不会在其他地方别用到,因此,未被用到的template parameter可以不具名,示例如下:

template <template <typename,typename=MyAllocator> class Container>
class Adaption{
    Container<int> storage;//等价于Container<int,MyAllocator>
    ...
};
  • Default Template Arguments(预设模板引数)

预设模板引数的设定大家可以参考这篇博客:default template arguments for both function and class templates
无论何种template parameters都可以有预设自变量,当然它必须匹配对应参数,很明显,预设模板自变量不能依赖于其自身参数,但可以相依赖于它之前声明的参数:

template <typename T,typename Allocator=allocator<T> >
class list;

和函数的预设自变量一样,某个参数带有预设自变量的条件是:后续所有参数也都有预设自变量。
后续参数的Usher自变量通常卸载同一个template声明语句中,但也可以写在该template更早的某个声明语句中。例如:

template <typename T1,typename T2,typename T3,typename T4=char,typename T5=char>
class Quintuple;//OK

template <typename T1,typename T2,typename T3=char,typename T4,typename T5>
class Quintuple;//OK

template <typename T1=char,typename T2,typename T3,typename T4,typename T5>
class Quintuple;//ERROR:T1不能拥有默认值,因为T2没有默认值

同时我们也不能重复指定默认模板引数

template <typename T=void>
class Value;

template <typename T=void>
class Value;//ERROR:预设自变量被重复定义了

Template Arguments(模板引数)

Template Arguments是编译器实例化一个template时用来替换template parameters的值。编译器以数种不同的机制来决定以何值替换template parameters:在带有参数P1,P2…的class template X作用域中,template X的名称与template-id

template <typename T>
inline T const& max(T const& a,T const& b){
    return a<b?b:a;
}
int main(){
    max<double>(1.0,-3.0);
    max(1.0,-3.0);
    max<int>(1.0,3.0);
}

但是需要注意有的template arguments无法被推导获得,我们最好将这一类参数放在template parameter list的最前面,这样客户端只需明白指定编译器无法推导的那些自变量即可,其余自变量仍可被自动推导获得:

template <typename DstT,typename SrcT>
//由于DstT不出现在自变量列,无法进行自变量推导
inline DstT implicit_cast(SrcT const& x){
    return x;
}
int main(){
    double value=implicit_cast<double>(-1);//OK,这样编译器可以自动推导SrcT参数
    return 0;
}

如果我们将template paramter的顺序换一下呢?

template <typename SrcT,typename DstT>
inline DstT implicit_cast(SrcT const& x){
    double value=implicit<int,double>(-1));
    return 0;
}

这样我们就需要将两个参数的类型明确给定了,因为SrcT在前面,在推导时候编译器可以确定,但是后面的需要给定默认的值,因此两种类型的值都必须明确指定。

由于function template可以被重载,因此及时写出一个function template的所有自变量,可能也不足以使得编译器确定到底该调用哪种函数,实例如下:

template <typename Func,typename T>
void apply(Func func_ptr,T x){
    func_ptr(x);
}

template <typename T>
void single(T);

template <typename T>
void multi(T);
template <typename T>
void multi(T*);

int main(){
    apply(&single<int>,3);//OK
    apply(&multi<int>,7);//ERROR:符合multi<int>形式的函数不只一个
    return 0;
}

不仅如此,明确指定template arguments还可能导致构建出不合法的C++类型,考虑如下的重载函数:

template <typename T> RT1 test(typename T::X const*);
template <typename T> RT2 test(...);

算是test<int>对于第一个function template而言是没有意义的,因为int类型并没与member type X,然而第二个function template没有这种问题,因此算式&test<int>可以明确制定出惟一一个函数地址。于是尽管第一个template以int替换失败,却并没有造成&test<int>随之不合法。正式SFINAE(替换失败并非错误)这一原则,function template的重载实际可行。看看SFINAE原则在如下代码中的应用:

template <int N>
int g(){
    return N;
}
template <int* P>
int g(){
    return *P;
}
int main(){
    return g<1>();//1无法适用于int*参数,SFINAE原则在这里起了重要的作用
    return 0;
}
  • Type template Arguments(型别引数)

template type arguments是针对template type parameters而指定的值,我们管用的大多数types都可以作为template arguments使用,但有两个例外:
1)local classes和local enum types不能作为template type arguments使用;
2)如果某个type涉及无名的class types或者无名的enum types,这样的type同样也无法作为template type arguments使用,但如果运用typedef使其具名便可以被当做template type arguments使用。

template <typename T>
class List{
    ...
};
typedef struct{
    double x,y,z;
}Point;
typedef enum{red,green,blue} *ColorPtr;

int main(){
    struct Association{
        int *p;
        int *q;
    };
    List<Association*> error1;//ERROR:template arugement不能为local type
    List<ColorPtr> error2;//ERROR:template argument不能为unnamed type
    List<Point> ok;//原本无名的type因typedef而有了名称
    return 0;
}
  • Nontype template Arguments(非类型引数)

Nontype template arguments是针对nontype template parameters(非类型模板参数)而指定的值。下面的nontype template arguments都合法:

template <typename T,T nontype_param>
class C;

C<int,33>* c1;

int a;
C<int*,&a) c2;

void f();
void f(int);
c<void(*)int,f>* c3;//调用f时候,&会被隐寓加入

class {
public:
    int n;
    static bool b;
};
C<bool&,X::b>* c4;//static类成员都可以被接受
C<int X::*,&X::n> c5;

template <typename T>
void templ_func();

C<void(),&templ_func<double> >* c6;

template arguments的一个一般性约束条件是:必须能够在编译期或者链接期求职。只在执行期间才能求值的表达式不能作为nontype template arguments使用;
及时如此,nontype template arguments还不包括:

null pointer常数
浮点数(floating-point numbers)
字符串字面常数(string literals)
derived-to-base转换

不能以字符串字面常量作为nontype template arguments的一个技术难题在于:两个内容相同的字符串字面常量可能存在两个不同的地址上,一种稍显笨拙的方法是引入一个字符串array:

template <char const* str>
class Message;
extern char const hello[]="Hello World!";
Message<hello>* hello_msg;

这里我们必须使用关键词extern,因为const array默认采用内部链接。
下面还有一些错误实例,供大家参考学习:

template <typename T,T nontype_param>
class C;

class Base{
public:
    int i;
}base;
class Derived:public Base{
}derived_obj;
C<Base*,&derived_obj>* error1;//ERROR,derived-to-base不被考虑;
C<int&,base.i>* error2;//ERROR,成员变数不被考虑
int a[10];
C<int*,&a[0]>* error3;//ERROR,不能使用array内某个元素的地址
  • Template Template Arguements(双重模板自变量引数)

Template template argument必须是这样的一个class template,其参数完全匹配待替换之template template parameter的参数。Template template argument的default template argument会被编译器忽略,除非对应的template template parameter有预设的自变量。

#include <list>
template <typename T1,typename T2,template <typename > class Container>
class Relation{
public:
    ...
private:
    Container<T1> dom1;
    Container<T2> dom2;
};
int main(){
    Relation<int,double,std::list> rel;//std::list的提供的template template argument有两个,而对应的template template parameter只有一个预设自变量。
    return 0;
}

问题出在std::list template拥有不止一个参数,第二参数(是个allocator,配置器)有默认值,但编译器把std::list匹配至Container时,该默认值被忽略了。
我们可以将其进行改写:

template <typename T1,typename T2,template <typename T,typename=std::allocator<T> > class Container>
class Relation{
public:
    ...
private:
    Container<T1> dom1;
    Container<T2> dom2;
};

template template parameter不能使用union和struct,猜猜看,template template argument呢?没有这个限定啦,所以开心的玩起来啦!

  • 等价

当两组template arguments的元素意义对等时候,我们称这两组变量等价。对于type arguments,typedef的名称并不印象对比过程最终被比较的是typedef所指代的type。对于整型nontype arguments,比较的是自变量值,与自变量表达式无关:

template <typename T,int I>
class Mix;
typedef int Int;
Mix<int,3*3>* p1;
Mix<Int,4+5>* p2;//p2和p1具有相同的类型

一个由function template产生的函数,和一个常规函数,无论如何不会被编译器视为等价,及时它们的类型和名称完全相同。这对class template造成两个重要结果:
1)由member function template产生的函数不会覆盖虚拟函数;
2)由constructor template产生的构造函数不会被当做default copy构造函数。同样的道理适用于由assignment template产生的assignment运算符不会被当做一个copy-assignment运算符。

Friends

Friend声明语句的基本概念简单:指定某些classes或functions,让它们可以对friend声明语句所在的class进行特权存取。但是下面两种事实使得这个概念变得复杂:
1)friend声明语句可能是某种物体的惟一声明(即friend仅仅在class内部声明,别无其他兄弟);
2)friend函数声明可以就是其定义。

friend class声明语句不能成为一个定义式,这就大大降低了问题的发生,涉及templates时,惟一需要考虑的情况是:你可以把某个class template的特定实体声明为friend:

template <typename T>
class Node;

template <typename T>
class Tree{
    friend class node<T>;
    ...
};  

注意:在class template的某一实体成为其他class或者class template的friend之前,class template Node必须已被声明而且可见,但是对于常规class而言并没有什么限制。

template <typename T>
class Tree{
    friend class Factory;//OK,因为这是常规类Factory的首次声明
    friend class Node<T>;//ERROR,因为之前并没有声明Node
};
  • Friend Functions

function template的具现体可以成为别人的一个friend function,前提是该function template名称之后必须紧跟着以角括号括起来的自变量列-如果编译器可推导出所有自变量,自变量列可以为空:

template <typename T1,typename T2>
void combine(T1,T2);

class Mixer{
    friend void combine<>(int&,int&);
    friend void combine<int,int>(int,int);
    friend void combine<char>(char,int);
    friend void combine<char>(char&,int>;//ERROR,char与char&不匹配
    friend void combine<>(long,long){...}//不能再此处定义函数
};

注意,我们无法定义一个template实体,最多只能定义一个特化体,因此一个令某实体获得名称的friend声明语句,不能是个定义式。如上面代码中的最后一个模板函数试图定义函数。

如果friend后面没有跟着角括号,有如下两种可能:
1)如果这不是个资格修饰名称(亦即不含::),就绝不会一弄某个template实体。如果编译器无法在friend声明处匹配一个non-template function,则这个friend声明便被当做这个函数的首次声明。这个声明语句是个定义式,即non-template function声明;
2)如果这是一个资格修饰名称(亦即含有::),就必定引用一个先前已定义的function或者functionn template。编译器会有限匹配常规non-template函数,然后才匹配function templates。这个friend声明语句不能是个定义式。

void multiply(void*);//常规函数

template <typename T>
void multiply(T);//function template

class Comrades{
    friend void multiply(int){}//定义了一个新函数::multiply(int)
    friend void ::multiply(void*);//引用先前定义的常规函数,即multiply(void*),而非multiply<void*>实体
    friend void ::multiply<double*>(double*);//带有角括号,此时编译器必须见到该template
    friend void ::error(){}//ERROR,带修饰符的friend,不能是个定义
};

上面代码中,我们将friend function声明在常规class中,如果把friend声明在class templates中,先前规则同样适用,而且template parameter可以参与到friend function之内:

template <typename T>
class Node{
    Node<T>* allocate();
    ...
};

template <typename T>
class List{
    friend Node<T>* Node<T>::allocate();
    ...
};

然而将friend function定义域class template中可以引发一个有趣的错误因为任何只在template中被声明的object,都是直到template被实例化后才能成为具现实体。

template <typename T>
class Creator{
    friend void appear(){
        ...
    }
};
Creator<void> miracle;
Creator<double> oops;//ERROR,试图再次生成::appear()

这种问题的解决方法是什么呢?确保class template的template parameters出现在定义于该template之中的所有friend function的类型之中,除非我们想要阻止这个class template在一个文件中被多次实例化,解决方法:

template <typename T>
class Creator{
    friend void feed(Creator<T>*){
        ...
    }
};
Creator<void> one;//OK,生成feed(Creator<void>*)
Creator<double> two;//OK,生成feed(Creator<double>*)

另外还请注意,这些函数被定义于class定义式内,因此它们案子成为inline。而且如果你在两个不同的编译单元中产生同一个函数,编译器都不会认为有错误。

  • Friend Template
class Manager{
    template <typename T>
    friend class task;
    template <typename T>
    friend void Schedule<T>::dispatch(Task<T>*);
    template <typename T>
    friend int ticket{
        return ++Manager::counter;
    }
    static int counter;
};

和常规的friend声明语句一样,只有当friend template产生一个无修饰函数名,而且该名称之后不紧跟着角括号,这个friend template才是一个定义式
friend template只能声明primary templates机器primary templates的成员。任何与primary template相关的偏特化体和明确特化体都将被编译器自动视为friends。

PS:
1)template template parameters(双重模板参数)是一种class templates占位符号,声明方式和class template类似。
2)primary templates指的是没有进行偏特化或者特化的模板类或者模板函数。

 类似资料: