C++程序设计语言Note3

二十、派生类

1.  引言(接口继承和实现继承;)
2.  派生类(继承、基类、派生类,可以将一个Derived*赋予一个Base*类型的变量而无须显式声明,而相反的转换必须是显式的;@1 成员函数:派生类的成员可以访问基类的公有和保护成员,但不可以访问基类的私有成员;@2 构造和析构函数:对象自底向上构造,析构自顶向下销毁,每个类都可以初始化成员和基类,类层次中的析构应该是virtual的,类层次中类的拷贝构造函数须小心使用以避免切片,虚函数调用的解析、dynamic_cast以及构造和析构中的typeid()反映了构造和析构的阶段;)
3.  类层次(一个派生类自身也可以作为其它类的基类,习惯称这样一组相关的类为类层次;@1 类型域:实现多态时要确定一个Base*类型的指针,它指向对象的真正类型,C++提供了四种解决方式:保证指针只能指向单一类型的对象、在基类中放置一个类型域供函数查看、使用dynamic_cast和使用虚函数,类型域技术需要手动写类型域区分方法和函数使用时的执行内容选择,不适合大型程序;@2 虚函数:虚函数机制允许程序员在基类中声明函数,然后在每个派生类中重新定义这些函数,从而解决了类型域方法固有问题;虚函数成员也称为方法,派生类函数参数类型须与基类一致,返回类型也只允许细微修改,首次定义虚函数的类必须定义它(除了纯虚函数),若派生类中一个函数名字与参数类型与基类中一个虚函数完全相同,则称它覆盖了虚函数的基类版本;多态和虚函数表vtbl;@3 显式限定:使用作用域解析运算符::调用函数(如Manager::print( ))能保证不使用virtual机制;@4 覆盖控制:在大型程序中,类层次复杂,不一定能确保正确覆盖,因此有virtual、=0、override和final等覆盖控制关键字;& 4.1override:可以用override关键字(其实是上下文关键字)来显式声明覆盖,说明符override出现在声明的最后;说明符override不是函数类型的一部分,而且在类外定义中不能重复;& 4.2final:在一个类层次中,我们可能希望派生类派生到某一个具体类时,希望用户阻止继续覆盖虚函数,则可以用final上下文关键字;通过在一个类名后加上final,我们可以将一个类的所有virtual成员函数都声明为virtual的;final说明符不是函数类型的一部分,在类外定义中不能重复使用;@5 using基类成员:函数重载不会跨作用域,当想选择版本时不会由派生类的同名成员函数隐藏基类中函数时,可以用using声明的方式将一个基类作用域中的成员函数加入到派生类定义中,可以用多个using声明将多个类的成员函数加入,不能用usiing指示将基类所有成员函数包含到派生类中;& 5.1继承构造函数:在没有添加新变量的情况下可以用using声明继承基类的构造函数;@6 返回类型放松:覆盖函数的类型必须与它所覆盖的函数类型一致,但C++提供了一种放松原则:如果原返回类型为B*,则覆盖函数的返回类型可以是D*,只要B是D的一个公有基类就可以,类似地,返回类型B&可以放松为D&,这一规则有时称为协变返回规则,这一返回规则只适用于返回时普通指针或引用的情况,不适用于智能指针;)
4.  抽象类(不能定义具体对象的类应该设计为抽象类,抽象类中至少含有一个纯虚函数,无法为抽象类实例对象,应该为抽象类定义一个虚析构函数和不为它定义构造函数(或声明为=delete),抽象类应该用作一个接口,当其被继承时若不覆盖其中的纯虚函数则派生的类也是一个抽象类,抽象类所支持的设计风格是接口继承,它与实现继承相对;)
5.  访问控制(public:可被任意函数访问,private:仅可被所属类的成员函数和友元访问,protected:可被所属类的成员函数和友元和派生类的成员函数和友元访问;@1 protected成员:将提供给派生类使用的函数声明为protected;& 1.1使用protected成员:在private和public之间,开放给派生类的函数声明为protected是最合适的;@2 访问基类:类似成员,基类也可以被声明为public、protected和private(或称被public、private和protected继承),private继承时基类B的公有和保护成员只能被D的成员函数和友元函数使用,protected继承时基类B的保护和公有成员只能被D的成员函数和友元函数以及D的派生类的成员函数和友元函数使用,public继承时基类B的公有成员可以被任何函数访问;& 2.1多重继承与访问控制:在一个多重继承框架中,如果通过多条路径都可以达到一个基类,则若任何一条路径中该基类可被访问,那么在派生类中此基类就可以被访问;即使单一实体有多条路径可达,我们也可以无二义性地访问它;@3 using声明与访问控制:using声明并不能用来获得额外的访问权限,它只是一种令可访问信息更便于使用的机制;)
5.  成员指针(可用->访问类成员;@1 函数成员指针:获得成员指针的方法是对一个完全限定的类成员名使用地址运算符如&X::f,可以使用形如X::*的声明符来声明“类X的成员指针”类型的变量(可以用using X_ptr=void (X::*)()的方式先为该指针类型起一个别名);成员m的指针可以与对象的混合使用,即可以使用运算符->*m_ptr和.*m_ptr运算符调用成员m;一个static成员不关联某个特定对象,因此static成员的指针就是一个普通指针;@2 数据成员指针:自然地,成员指针的概念可以被应用于数据成员和带参数及返回类型的成员函数;@3 基类和派生类成员:我们可以将一个基类成员指针安全地赋给派生类成员指针,但反方向赋值不行,这称为逆变性;我们可以将一个派生类的指针赋予其基类指针,这与成员指针赋值方向相反;)

二十一、类层次

2.  设计类层次(用“虚拟用户界面系统”来讲解类层次技术;@1 实现继承:第一个方案是使用实现继承的类层次;lval_box类;& 1.1评价;@2 接口继承:将lval_box作为一个纯接口类,然后用具体的类来多重继承lval_box和BBwidget;@3 替代实现方式:我要解决版本控制的问题,可以先从lval_box派生出抽象的lval_slider,然后再和BBwidget、CWwidget不同版本派生出不同版本的lval_slider;& 3.1评价:用抽象类实现接口继承,用带有虚函数实现的基类支持实现继承;@4 定位对象创建:工厂类,如lval_maker为lval_box类家族的每个接口都提供了一个创建对象的函数,它的函数称为虚构造函数(构造lval_box的,不是本身),可以用lval_maker派生出各种版本的具体maker类;)
3.  多重继承(继承可以实现共享接口(接口继承)和共享实现(实现继承);@1 多重接口:抽象类是最容易想到表示接口的方式;@2 多重实现类:Satellite和Displayed类共同派生出Comm_sat类,这样可以使在使用时不用写那么多转发函数;@3 二义性解析:两个基类中可能含有同名的成员函数,可以用前面加类限定符的方法显式声明调用哪一个基类的成员函数,也可以在派生类中先为它起一个别名;可以用添加中间类的方法事先将同名的成员函数包含在一个声明为virtual的函数内被调用;@4 重复使用基类:如果B继承A,C继承A,D继承B和C,则默认A会为产生B和C各产生一份拷贝,可以用B::和C::的方式限定不同继承路径的函数;@5 虚基类:当你想要D从B和C继承的A是同一份拷贝时,需要使基类A是虚基类,即声明时有class B:public virtual A和class C:public virtual A;& 5.1构造虚基类:无论程序多么复杂,虚基类声明也能保证它的只构造函数只被调用一次,且每个虚基类构造函数都由完整对象的构造函数(最终派生类的构造函数)负责(显式或隐式地)调用,这确保在类层次中虽被多次提及,但它只被构造一次;& 5.2一次只调用一个虚基类成员:在使用虚基类时,用带限定符的显式调用(比如A::f( ))不会用到虚调用机制,相反,它直接调用显式具名函数;@6 重复基类与虚基类:重复基类与虚基类实现性能差不多,重复基类不需要维持共享;& 6.1覆盖虚基类函数:若不同的类覆盖了同一函数(如B覆盖了A中f,C也覆盖了A中f),则该层次关系存在错误,这时D应该为该函数提供覆盖版本;为虚基类提供一部分实现但非全部实现的类称为混入类;)

二十二、运行时类型信息

2.  类层次导航(对于类似lval_box的类,一个合理应用是将其实例对象作为小部件或控件传递给控制屏幕的系统,当有动作发生时,再将对象回传给应用程序,但对于我们传递给系统、随后又传回给我们的对象,其类型信息丢失了,为了恢复“丢失的”对象类型信息,我们需要用某种方法要求对象透露其类型,因此在运行时检测对象类型的最明显也最有用的操作就是类型转换,若对象为预期类型,该操作应该返回一个合法的指针,否则返回一个空指针,RTTI;@1 dynamic_cast:dynamic_cast<T*>§检查p指针指向的对象,若对象类型为T或其类型有唯一基类T,则dynamic_cast返回一个指向该对象的T*类型的指针,否则返回nullptr;若p指向的对象包含不止一个表示基类型T*的子对象,则会由于不满足唯一识别的要求,转换失败并返回nullptr;& 1.1用dynamic_cast转换引用类型:当用dynamic_cast来检查指针p时,首先要考虑它本身是否是一个空指针,但对于引用,我们可以确定它一定指向一个对象,所以用dynamic_cast<\T&>®时当引用对象不是所期望的类型时,它会返回一个bad_cast异常,所以和指针的if…else不同,对于引用应该用try…except的方式来处理;@2 多重继承:多重继承时dynamic_cast对于虚基类和非虚基类有不同表现,当基类为虚基类时,该指针可以被RTTI接受类型,但当基类不是虚基类而是重复继承时,RTTI类型转换失败会得到一个nullptr;@3 static_cast和dynamic_cast:dynamic_cast可以从一个多态虚基类转换到一个派生类或者兄弟类,static_cast则不行,因为它不检查要转换的对象;dynamic_cast要求运算对象是多态的,因为它需要特定的信息来找到表示基类的子对象;对于void * 的对象,只能用static_cast;dynamic_cast和static_cast都遵循const的访问规则;@4 恢复接口:从设计的角度看,dynamic_cast可以被看作一种询问对象是否提供了指定接口的机制;)
3.  双重分发和访客(动态分发:基类指针根据虚函数表解析执行恰当的虚函数;限制:不能根据两个动态类型来选择函数,而且,虚函数必须是成员函数,这要求当想加入一个虚函数到类层次中时必须修改接口基类和其派生类;@1 双重分发:如当定义shape &s1,shape &s2时,调用s1.intersect(s2)时,基本策略是调用一个虚函数为s1选择正确的函数,然后再进行一次调用来为s2选择正确的函数;但要注意仔细观察,这种是否是必要,可否通过单重分发实现;查询表的方法解决双重分发;@2 访客:可以对一个结点层次和一个操作层次进行双重分发,为正确的结点选择正确的操作,操作被称为访客,访客都被定义再类visitor派生类中,结点层次中每个结点都是一个类,都定义了虚函数accept(),接受参数visitor&;)
4.  构造和析构(构造操作自顶向下,析构操作自底向上,编程时依赖构造和析构顺序是不明智的,但是可以通过在对象尚未构造完成的某个点调用虚函数、dynamic_cast或typeid来观察这种顺序,在构造函数未完成时的某个点,对象的(动态)类型信息仅反映当前已经构造完成的部分;)
5.  类型识别(typeid()可以获知一个对象的确知类型,它生成一个对象,表示它处理的类型,typeid()返回一个指向标准库类型type_info的引用;若typeid()的运算对象是一个值为nullptr的多态类型指针或引用,它会抛出一个std:bad_typeid异常,若typeid()运算对象不是多态类型或不是一个左值,则结果在编译时即可知,无需运行时对表达式求值;若运算对象是一个多态的解引用指针或引用,则返回的type_info对应对象的最底层派生类,即定义对象时使用的类型;@1 扩展类型信息:type_info只保存着最少的类型信息,但对于一个具体的类,它的扩展信息可能与其它类不同,当我们用一种方法获得该类的扩展类型信息时,可以用map将其和type_info存在一起,这样方便用户代码查找扩展类型信息;)
6.  RTTI的使用和误用(应该在必要时才使用显式运行时类型信息,静态类型检查更安全,开销更小,应避免使用多次的typeid运算和比较操作(例如稍加伪装的switch语句);不要为了使用RTTI而使原本不需要抽象的基类变为抽象基类,这时可以用容器模板解决;)

二十三、模板

2.  一个简单的字符串模板(前缀template<typename C>指出将要声明一个模板,而在声明中将要用到类型参数C;@1 定义模板:有类模板和函数模板,类模板成员的声明和定义与非模板类成员完全一样,模板成员不必定义在模板类中,也可以在外部定义,模板类成员本身也是模板,通过所属模板类的参数进行初始化,因此,在模板类外部定义一个成员时,必须显式声明模板;@2 模板实例化:从一个模板和一个模板实参列表生成一个类或一个函数的过程通常被称为模板实例化,组合使用模板和简单内联可以消除很多直接或间接的函数调用;)
3.  类型检查(在模板实例化过程中,类型检查由编译器来做,并且不能由程序员额外限定;谓词(Predicate)和概念(concept);@1 类型等价:对一个模板使用相同的模板实参,可以得到相同的生成类型,且用using Uchar=unsigned char之后用unsigned char和Uchar生成的string是同一类型;编译器可以对常量表达式求值;对一个模板使用不同的模板实参生成的类型是不同类型,特别是,用相关实参生成的类型不一定是相关的;@2 错误检测:不同的错误检查出来时期也不同;实例化点,类型检查有可能比较晚,会在链接时才做;)
4.  类模板成员(与普通类一样,模板类可以有几种不同类型成员:数据成员、成员函数、成员类型别名、static成员、成员类型和成员模板;@1 数据成员:和普通类一样,类模板可以有任意类型的数据成员,非static数据成员可以在其定义时初始化,也可以在其构造函数中初始化,非static数据成员可以是const的,但不能是constexpr的;@2 成员函数:和普通类一样,非static成员函数定义可以在类模板内部,也可以在外部,类模板成员函数可以是virtual的,也可以不是,但是一个虚成员函数名不能再用作一个成员函数模板名;@3 成员类型别名:可以使用using或者typedef向类模板引入成员类型别名;@4 static成员:一个类外定义的static数据或函数成员在整个程序中只能有唯一一个定义;与非模板类一样,const或constexpr static的字面值常量类型数据成员可以在类内初始化,不必在类外定义;一个static成员只有真被使用时才需要定义;@5 成员类型:可以将类型定义为类模板的成员,成员类型可以是一个类或者一个枚举,成员枚举可以在类外定义,但在类内声明中必须给出其基础类型;@6 成员模板:一个类或者一个类模板可以有模板成员,这使得我们表示相关类型时能得到满意的控制度和灵活性;& 6.1模板和构造函数:若自己不定义,则编译器会自动生成默认构造函数、默认拷贝构造函数等;& 6.2模板和virtual:成员模板不允许virtual限定,即不允许虚模板;& 6.3使用嵌套:一个模板成员依赖于所有模板实参,当成员的行为实际上并未使用所有模板实参时,这种依赖就会不幸产生副作用,因此在模板中尽量避免嵌入类型除非它们真正依赖于所有模板参数;@7 友元:模板类可以将函数指定为friend,友元函数名后<>是必需的,它清楚地指出友元函数是一个模板函数,若没有<>,则友元函数将被认为是非模板函数,与成员函数类似,友元函数只有使用时才会被实例化;类似普通类,类模板也可以指定其它类为friend;友元关系既不能继承也不能传递;我们不能直接将一个模板定义为一个类的友元,但我们可以将一个友元声明改为一个模板;)
5.  函数模板(当调用一个函数模板时,函数实参类型决定了使用哪个模板版本,即,模板实参是从函数实参推断出来的;@1 函数模板实参:编译器可以从一次调用中推断类型和非类型模板实参,前提是函数实参列表唯一标识出模板实参集合;类模板参数并不是靠推断来确定的,但类模板可以依赖特例化机制在可选的定义中隐式地进行选择;若想由基于推断出的类型创建一个对象,常用的方法是通过调用一个函数来进行推断(以及对象创建);如果不能从函数实参推断出一个模板实参,我们就必须显式指定它,这与模板类显式指定模板实参的方法一样;@2 函数模板实参推断:若一个模板函数实参的类型是书中表所示结构的组合,则编译器可以从此函数实参推断出一个类型模板实参T或TT,或是非类型模板实参I;& 2.1引用推断:对左值和右值采取不同的处理措施有时很有必要,模板实参推断是区分左值和右值的,X类型的左值会被推断为一个X&,而右值被推断为X,这与非模板实参的处理不同,值会绑定到非模板实参右值引用,但这一规则对实参转发非常有用(能够接受右值时移动,接受左值时拷贝);@3 函数模板重载:我们可以声明多个同名的函数模板,甚至函数模板和普通函数也可以同名;& 3.1二义性消解:可以通过显示限定模板实参消除二义性,也可以通过添加适当的声明,即增加一些重载的普通函数;& 3.2实参代入失败:当对函数模板的一组实参查找最佳匹配时,编译器会检查实参的使用是否符合函数模板完整声明(包括返回类型)的要求,如template<typename Iter> typename Iter::value_type mean(Iter first,Iter last),当给mean传入指针而非一般的iterator时,会由于指针没有value_type而匹配失败,但这种代入失败并不是错误,它会导致该模板函数被忽略,继续寻找其它匹配;& 3.3重载和派生:重载解析规则能保证函数模板能完美地和继承机制结合使用,当函数模板的函数参数接受基类指针时,传入派生类指针将会发生隐式的由派生类指针到基类指针的转换;& 3.4重载和非推断的参数:对于在模板参数推断中未用到的函数实参,其处理方式与非模板函数实参完全相同,特别是,常用类型转换规则仍然有效,有时这种语法被称为显式特例化;)
6.  模板别名(我们可以用using语法或typedef语法为一个类型定义别名,using语法更常用,一个重要原因是它能为模板定义别名,模板的一些参数可以固定;可以绑定一个偏特化的模板,也可以绑定一个特例化的模板;不能定义别名的特例化版本;)
7.  源码组织(模板组织源码三种方式:一个编译单元中使用前定义,一个编译单元中使用前声明而后定义,一个模板中使用前声明但在其它编译单元中定义,C++不支持第三种;@1 链接:模板链接规则实际上是生成的类和函数的链接规则,这意味着若一个类模板布局或一个内联函数模板定义改变,则所有使用其的代码都要重新编译;可以将模板封装在非模板接口函数中来降低复杂模板库中代码修改带来的风险;)

二十四、泛型程序设计

1.  引言(模板提供了传递类型参数而不丢失信息的能力,推迟的(实例化时)类型检查,传递常量参数的能力(可以在编译时计算);模板的首要用途是支持泛型程序设计,它提供了(编译时)参数化多态;更多地关注代码生成技术(将模板看作类型和函数的生成器),并依赖类型函数表示编译时计算的编程方式被称为模板元程序设计;模板类型检查是在模板定义时检查实参的使用,而不是(在模板声明中)检查显式的接口;泛型程序设计的提升和概念;)
2.  算法和提升(函数模板就是普通函数的泛化,它能对多种数据类型执行动作,并且能用以参数方式传递来的各种操作实现要执行的操作;算法就是一个求解问题的过程或公式,通过一个有穷的计算序列生成结果,因此函数模板常称为算法;当需要将一个特定数据类型上执行特定操作的函数泛化为一个在多种数据类型上执行更通用操作的算法时,最有效的方法是从一个(多个可能更好)具体实例来泛化出一个好的算法,这种泛化过程就成为提升;)
3.  概念(概念就是模板对实参的要求的集合,使之能用于很多模板和很多实参类型;@1 发现概念:在设计概念时,用三阶段分析方法:首先考察模板的最初实现,确定它使用了参数类型的哪些属性(操作、函数、成员等等),并确定这些操作的含义,确定这个特定模板对其实参的最小要求;接下来考察其它的模板实现,列出它们对自己模板实参的要求,这样可以选择一个适用性更强的要求更少/更简单的实现;考察最终结果列表与我们用过的其他模板的要求(概念)列表间的异同,尝试优选一些简单的公共概念,使之能表示本来很长的要求列表;@2 概念和约束:概念不是任意的属性集合,大多数类型的属性列表并不能给出一个一致、有用的概念定义,要成为有用的概念,要求列表必须反映模板类的一组算法或一组操作的需求;有一些模板被编写出来,但它们并不能很好地反映通用算法或广泛应用的类型,这些模板是为特定实现中的特定用途而设计的,这种模板实参的要求称为约束或特殊概念;)
4.  具体化概念(C++没有提供专门的语言特性来直接表达概念,一个概念可以用一个谓词表示,可将概念看作一个编译时函数,检查一组模板实参,当实参满足概念要求时返回true,否则返回false,因此可以将概念实现为一个constexpr函数,用数于约束检查表示对constexpr谓词的一次调用,它检查一组类型和值是否符合概念,可以用static_assert()或如templated<Predicate C>的形式来检查,约束检查有其缺点;@1 公理:讨论模板实参要求时,用‘公理’表示语义属性,我们使用公理描述一个类或算法对其输入集合有何假设,一个公理无论是如何表达的,都表示一个算法或类对其实参的期望(假设);@2 多实参概念:当考虑一个单实参概念并将其用于一个类型时,看起来像类型检查,概念就像类型的类型;对于某些接受多实参的模板,需要定义多实参之间的关系的约束和概念;@3 值概念:概念可以表达对一组模板实参的任何(语法)要求,模板实参可以是一个整型值,因此概念也可以约束整型实参,即让该作为模板参数传入的整型值满足某一条件;@4 约束检查:书籍的配套网站有一些常用的约束检查如Input_Iterator<X>、Regular<X>等;@5 模板定义检查:约束检查只对模板实参检查实参是否满足模板要求,并不一定保证在模板内使用了超出约束检查的特性时能够无异常,这时需要再寻找一个合适的提供所需属性的类型或者自己定义一个;)

二十五、特例化

2.  模板参数和实参(模板可以接受:类型类型的参、内置类型值参数(如int值或函数指针)和模板类型的模板参数;一种很常见的模板类型实参命名是采用首字母大写的短名字;@1 类型作为实参:可以用typename和class前缀将一个模板实参定义为类型参数,当用作模板实参时,用户自定义类型和内置类型是完全相同的;一个类型必须在作用域内且可访问,才能作为模板实参;@2 值作为实参:非类型或模板的模板参数称为值参数,传递给它的实参称为值实参,例如可用整数实参提供大小和限制(Buffer和array);值实参可以是整型常量表达式、外部链接的对象或函数的指针或引用、指向非重载成员的指针和空指针;字符串字面值常量和浮点数值不能作为模板实参,整数模板实参必须是常量;在模板参数列表中,一个类型模板参数出现后即可当作一个类型来使用;@3 操作作为实参:可以用一个模板值实参或者一个类型实参来传递操作实参,常用的是传一个类型实参作为操作实参,可以传函数对象、函数指针或lambda表达式,lambda表达式可以用一个名字代替;@4 模板作为实参:有时传递模板实参(而不是类或值)作为模板实参很有用;为将一个模板声明为模板参数,我们必须指定其所需的实参;只有类模板可以作为模板实参;对于模板需要一两个容器的情形,可以不用传模板作为实参,可以直接传容器类型;@5 默认模板实参:可以像默认函数实参一样为尾部模板参数指定和提供默认实参;& 5.1默认函数模板实参:默认模板实参也可以用于函数模板;)
3.  特例化(用户自定义特例化:为一个模板提供提供可选的多个定义,令编译器能根据用户使用模板时提供的实参来选择使用哪个定义;完整特例化:使用该版本的特例化时不用再指定或推断任何模板参数;模式中包含模板参数的特例化版本称为部分特例化;类型擦除;@1 接口特例化:有时候特例化并非为了优化算法而是为了修改接口(乃至表示);& 1.1实现特例化:特例化可用来为特定模板参数集合提供可选的类模板实现,在这种情况下,特例化版本可以提供与通用模板不同的实现方式;@2 主模板:当同时拥有一个模板的通用定义及其针对特定模板实参集合定义的特例化实现版本时,我们称最通用的模板为主模板;主模板为所有特例化版本定义了接口,即主模板用来确定某个模板的使用是否合法,并参与重载解析;主模板必须在任何特例化版本前声明,为了定义特例化版本我们给出主模板的声明就够了,如果程序中有使用主模板则我们需要定义其,否则可以不定义主模板;@3 特例化顺序:一个特例化版本比另一个版本更特殊是指与其特例化模式匹配的所有实参列表也都与另一个版本的特例化模式匹配,但反之不成立;在对象、指针等的说明中,最特例化的版本优先于其他版本被优先采纳;@4 函数模板特例化:C++的函数可以重载,所以考虑特例化较少;C++仅支持函数模板完全特例化,对于部分特例化的,可以考虑函数重载;& 4.1特例化与重载:定制点;vector的sort与less函数示例;& 4.2非重载的特例化:从技术角度看,特例化和重载的区别就是所有函数个体都可以参与重载,而只有主模板可以特例化;函数特例化用途有限,其中一个用途是在无实参的函数中进行选择;)

二十六、实例化

1.  引言(模板提供了灵活的代码组织机制,编译器从模板定义及其词法环境、模板实参及其词法环境和模板使用环境来组合代码(信息);本章处理名字绑定;)
2.  模板实例化(从一个类模板和一组模板实参,编译器应生成一个类定义并为程序中用到的成员函数生成定义,从一个模板函数和一组模板实参,编译器应该生成一个函数,此过程通常称为模板实例化;生成的类和函数称为特例化,编译器生成的为生成特例化,程序员显式编写的为显式特例化,又称为用户自定义特例化;编译器根据名字绑定规则为用到的模板生成类和函数;@1 何时需要实例化:只有在需要类定义时才必须生成类模板的特例化版本,特别是,如果只是为了声明类的指针,是不需要实际的类的定义的;模板使用之处定义了一个实例化点;对于一个模板函数,只有当它被使用时,才需要一个函数实现来实例化它;实例化一个类模板并不意味着要实例化它的所有成员函数;@2 手工控制实例化:一个显式实例化请求在语法上就是一个特例化声明加关键字template前缀,模板声明以template<开始,而简单的template则表示一个实例化请求的开始;与模板函数调用类似,我们可以忽略可从函数实参推断出的模板实参;当显式实例化一个模板时,它的所有成员函数也同时被实例化;相同的特例化有两个定义是编译错误,C++标准不要求编译器检测分散在分离编译单元中的多重实例化;作为显式实例化请求的补充,C++语言还提供了显式不实例化请求(通常称为外部模板,extern template),其用途在于,在某个编译单元中对一个特例化版本进行显式实例化,当在其它编译单元中使用此版本时,使用extern template;)
3.  名字绑定(我们在定义模板函数时应尽量降低对非局部信息的依赖,原因在于模板会在未知上下文中基于未知类型生成函数和类,因此我们尽可能地令模板定义是自包含的,将本来是全局上下文中的实体以模板实参的形式提供;但有时为了给模板一个最精炼的实现,有时我们必须使用一些非局部名字;一些具有常规名字和语义的操作,可能是在其它局部作用域内重载或重定义的非局部名字,要注意;为模板显式或隐式使用的每个名字寻找其声明的过程称为名字绑定;模板名字绑定的普遍问题是模板实例化涉及3个上下文,C++语言将模板定义中使用的名字分为依赖性名字和非依赖性名字两类;@1 依赖性名字:我们称一个函数调用依赖一个模板参数当且仅当满足下列条件:根据类型推断规则,函数实参类型依赖于一个模板参数T,或根据类型推断规则,函数有一个参数依赖于T。大体上,如果被调用函数实参或形参明显依赖于模板参数,则函数名是依赖性名字,如果一个函数调用(只会)碰巧有一个实参与实际的模板参数类型匹配,则它不是依赖性的;默认情况下,编译器假定依赖性名字不是类型名,因此,为了使依赖性名字可以是一个类型,你必须用关键字typename显式说明,类似地,可以引入类型别名来避免使用typename的尴尬;命名.(点)、->或::后面的成员模板需要使用关键字template;@2 定义点绑定:当编译器遇到一个模板定义时,它会判断哪些名字是依赖性的,如果名字是依赖性的,编译器将查找其声明的工作推迟到实例化时,编译器将不依赖于模板实参的名字当作模板外的名字一样处理,因此在定义点位置这种名字必须在作用域中;如果找到了这个名字的声明,则编译器就会使用这个声明,即使后面发现“更好的”声明;@3 实例化点绑定:确定依赖性名字含义所需的上下文由模板的使用(给定一组实参)决定,这被称为此特例化版本的实例化点,模板对一组给定模板实参的每次使用都定义了一个实例化点,对一个函数模板而言,此位置位于包含模板使用的最近的全局作用域或名字空间作用域中,恰好在包含此次使用的声明之后;实例化点在被调用的局部作用域之外,它保证内部调用的函数是全局的;为了允许递归调用,函数模板的实例化点位于实例化它的声明之后;对一个模板类或一个类成员而言,实例化点恰好位于包含其使用的声明之前;依靠模板实参来显式化依赖关系简化了我们对模板代码的思考,更使我们能访问局部信息;@4 多实例化点:编译器会在以下位置为模板生成特例化版本:任何实例化点、任何编译单元的末尾或为生成特例化而特别创建的编译单元中,如果一个程序用相同的模板实参组合多次使用一个模板,则模板有多个实例化点,如果选择不同的实例化点可能导致两种不同的含义(不同的匹配),则程序是非法的;@5 模板和名字空间:当函数被调用时,即使其声明不在当前作用域中,只要它是在某个实参所在的名字空间中声明的,编译器就能找到它;编译器完成依赖性名字的绑定是通过查看模板定义处所用域的名字和依赖性调用的一个实参空间的名字空间中的名字来实现的;@6 过于激进的ADL:实参依赖查找(argument-dependent lookup,ADL)对避免冗长代码很有用处;有时ADL可以做诸如根据<<运算符找到endl名字的工作,但有时在与未受限模板组合使用时,ADL可能显得“过于激进”了,有可能在标准库中找到同名算法但你其实想表达是使用局部作用域内的同名函数;解决方法一是通过为函数调用显式指定命名空间,二是使用using声明使得编译器在重载解析时考虑特定的版本;@7 来自基类的名字:如果一个类模板有一个基类,则它可以访问来自基类的名字;与其它名字类似,有两种不同的可能:基类依赖于一个模板实参,基类不依赖于模板实参,第二种情况就按非模板类中的基类那样处理即可,而对于依赖模板参数的基类,则有时需要显式指定,这是因为依赖的名字在使用时才进行名字绑定;有三种方式实现使来自一个依赖性的名字被考虑:用显式指定依赖性类型限定名字,声明一个名字指向此类的一个对象,用using声明将名字引入作用域;)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值