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

7 篇文章 0 订阅

参数化声明

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指的是没有进行偏特化或者特化的模板类或者模板函数。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++ Template》第二版,2017年9月16日出版 Templates are among the most powerful features of C++, but they remain misunderstood and underutilized, even as the C++ language and development community have advanced. In C++ Templates, Second Editi on, three pioneering C++ experts show why, when, and how to use modern templates to build software that’s cleaner, faster, more efficient, and easier to maintain. Now extensively updated for the C++11, C++14, and C++17 standards, this new edition presents state-of-the-art techniques for a wider spectrum of applications. The authors provide authoritative explanations of all new language features that either improve templates or interact with them, including variadic templates, generic lambdas, class template argument deduction, compile-time if, forwarding references, and user-defined literals. They also deeply delve into fundamental language concepts (like value categories) and fully cover all standard type traits. The book starts with an insightful tutorial on basic concepts and relevant language features. The remainder of the book serves as a comprehensive reference, focusing first on language details and then on coding techniques, advanced applications, and sophisticated idioms. Throughout, examples clearly illustrate abstract concepts and demonstrate best practices for exploiting all that C++ templates can do. Understand exactly how templates behave, and avoid common pitfalls Use templates to write more efficient, flexible, and maintainable software Master today’s most effective idioms and techniques Reuse source code without compromising performance or safety Benefit from utilities for generic programming in the C++ Standard Library Preview the upcoming concepts feature The companion website, tmplbook.com, contains sample code and additional updates.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值