| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> Effective C++ 55个具体做法 (Meyers) 7. 模板和泛型编程 摘录 -> 正文阅读 |
|
[C++知识库]Effective C++ 55个具体做法 (Meyers) 7. 模板和泛型编程 摘录 |
C++ templates的最初发展动机很直接:让我们得以建立“类型安全(type-safe)的容器”,如vector、list和map。最终人们发现,C++ templates机制自身是一部完整的图灵机:它可以被用来计算任何可计算的值。于是导出了模板元编程(template metaprogramming),创造出“在C++编译器内执行并与编译完成时停止执行”的程序。 条款41: 了解隐式接口和编译器多态面向对象编程总是以显示接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。比如:
我们可以这样说doProcessing的w: ? ? ? ? 可以在源码中找出这个接口,看看它是什么样子,所以我们称此为一个显式接口(explicit interface)。 ? ? ? ? 由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态(runtime polymorphism)。 Templates及泛型编程的世界,与面向对象有根本上的不同。与此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit interface)和编译器多态(compile-time polymorphism)移到前头了。
现在我们在看看doPrcessing内的w? ? ? ? ? w必须支持一种接口,系由template中执行与w身上的操作来决定的。本例看来w的类型T必须支持size,normalize和swap成员函数,copy构造函数,不等比较。这一组表达式(对此template而言必须是有效编译)便是T必须支持的隐式接口。 ? ? ? ? 凡是涉及到w的任何函数调用,例如operator>和operator!=,有可能造成template具现化(instantiated),使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates”会导致调用不同的函数,这边是所谓编译期多态(compile-time polymorphism)。 通常显式接口由函数的签名式(也就是函数的名称、参数类型、返回类型)构成。例如
隐式接口就完全不同了。它并不急于函数签名式,而是由有效表达式(valid expression)组成。
T(w的类型)的隐式接口看起来好像有这些约束: ? ? ? ? 它必须提供一个名为size的成员函数,该函数返回一个整数值; ? ? ? ? 它必须支持一个oprator!=的函数,用来比较两个T对象。 真要感谢操作符重载(overload)带来的可能性,这两个约束都不需要满足。 隐式接口仅仅是由一组有效表达式构成,表达式自身可能看起来很复杂,但它们要求的约束条件一般而言相当直接又明确。 请记住:class和template都支持接口(interface)和多态(polymorphism)。 对class而言接口是显式的,以函数签名为中心,多态则是通过virtual函数发生在运行期。 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译器。 条款42: 了解typename的双重意义
比较喜欢typename,因为它暗示参数并非一定得是个class类型。而在只接受用户自定义类型时保留旧式的class。 然而C++并不总是将class和tyepname视为等价。
template出现的名称如果依赖于某个tempplate参数,称之为从属名称(dependent names),并不依赖则成为非从属名称(independent name)。如果从属名称在class内嵌套状,称它为嵌套从属名称(nested dependent name)。iter就是一个嵌套从属名称。 嵌套从属名称有可能导致解析(parse)困难。比如:
看起来我们声明x为一个local变量,它是个指针,指向C::const_iterator。但如果C::const_iterator不是个类型?如果C有个static成员变量而碰巧命名为const_iterator,或x碰巧是个global变量名称?那么上述代码是一个相乘动作。 C++有个规则可以解析此一歧义状态:如果解析器知道template中遭遇一个嵌套从属名称,它便假设这个名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。我们必须告诉C++说C::const_iterator是个类型。只要紧邻它之前放置关键字typename即可。
一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧邻它之前一个位置放上关键字typename。 typename只被用来验明嵌套从属类型名称:其他名称不该有它的存在。例如下面的function template,接受一个容器和一个指向该容器的迭代器;
typename必须作为嵌套从属类型名称的前缀词这一规则的例外是typename不可以出现在base class list内的嵌套从属类型名称之前,也不可以出现在member initialization list中作为base class修饰符。
看一下最后一个typename例子;
value_type被嵌套在iterator_traits<IterT>之内而IterT是个template参数,所以我们必须在它之前放置typename。 如果认为多打几次这个字实在很恐怖,那么你应该会想到建立一个typedef。
请记住;声明template参数时,前缀关键字class和typename可互换。 请使用关键字typename标识嵌套从属类型名称;但不得在base class list或member initialization list内以它作为base class修饰符。 条款43: 学习处理模板化基类内的名称撰写一个程序,它能够传达信息到若干个不同的公司去。
假设我们有时候想要在每次送出信息时log某些信息。drived class可轻易加上这样的生产力,那么;
问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的MsgSender<Company>,但其中的Compant是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么--更明确地说没办法知道它是否有个sendClear函数。 为了让问题更具体化,假设我们有个class CompanyZ坚持使用加密通讯:
注意cass定义式最前头的“template<>”语法象征这既不是template也不是标准class,而是个特化版本的MsgSender template,在template实参是CompanyZ时被使用。这是所谓模板全特化(total template specialization):template MsgSender针对类型CompanyZ特化了,而且其特化时全面性的,也就是说一旦类型参数被定义为CompanyZ,在没有其他template参数可供变化。
因为C++知道base class template有可能被特化,而那个特化版本可能不提供和一般性template相同的借口。C++往往拒绝在template base class内寻找继承而来的名字。然而就某种意义而言,当我们从Objected Oriented C++ 跨进Template C++,继承就不像以前那般畅行无阻了。 我们必须有某种办法令C++不进入templatized base classes观察的行为失效。有三个办法,第一是在base class函数调用之前加上“this->”:第二是使用using声明式。并不是base class名称被derived class名称遮掩,而是编译器不仅如此base class作用域内查找,于是我们通过using告诉它,请它这样做。第三个做法是,明白指出被调用的函数位于base class内,但这往往是最不让人满意的一个解法,因为如果被调用的是virtual函数,上述的明确资格修饰会关闭virtual绑定行为。 从名称可视点的角度出发,上述每一种解法做的事情都相同:对编译器承诺base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口。
本条款探讨的是,面对“指涉base class member”之无效reference,编译器的诊断时间可能发生在早期(当解析derived class template的定义式时),也可能发生在晚期(当那些template被特定值template实参具现化时)。C++的政策是宁愿早诊断,这就是为什么“当base class从template中被具现化时”它假设它对那些base class的内容毫无所悉的缘故。 请记住;可在derived class template内通过this->指涉base class template内的成员名称,或藉由一个明白写出的base class资格修饰符完成。 条款44: 将与参数无关的代码抽离templatestemplate是节省时间和避免代码重复的一个好方法。不过使用template可能会导致代码膨胀(code bloat):其二进制码带着重复(或几乎重复)的代码、数据、或二者。 代码template带来的代码膨胀主要工具为:共性和变性分析(commonality and variability analysis)。 当两个函数有些代码重复,我们会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。 在编写template时,重复是隐晦的:毕竟只有只存在一份template源码,所以你必须训练自己去感受template被具现化多次时可能发生的重复。 比如,为固定尺寸的正方形矩阵编写一个template
这会具现两份invert。 如果我们看见两个函数完全相同,数值不同。我们会建立一个带数值参数的函数。
但还有一个棘手的问题没有解决,SquareMatrixBase::invert如何知道该操作什么数据?虽然它每从参数中知道矩阵尺寸,但它知道那个特定矩阵的数据在哪儿?想必只有derived class知道。Derived class是如何联络其base class做逆运算动作? 一个可能的做法为SquareMatrixBase::invert添加另一个参数,也许是个指针,指向一块用来放置矩阵数据的内存起始点。这似乎不方便! 另一个办法是令SquareMatrixBase储存一个指针,指向矩阵数值所在的内存,而只要存储了那些东西,也就可能知道矩阵尺寸。
这种类型的对象不需要动态分配内存,但对象自身可能非常大。另一种做法是把每一个矩阵的数据放进heap。
代价就是硬绑着矩阵尺寸的那个invert版本,有可能生成比共享版本(其中尺寸仍然以函数参数传递或存储在对象内)更佳的代码。 从另一个角度看,不同大小的矩阵只拥有单一版本的invert,可减少执行文件大小,也就是降低程序的working set(指一个在虚拟环境下执行的进程而言,其所使用的那一组内存页)。 你越是尝试精密做法,事情变得越是复杂。 本条款只讨论由non-type parameters带来的膨胀,其实type parameter也会导致膨胀。 请记住;Template生成的多个class或函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量代替template参数。 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述(binary representations)的具现类型(instantiation types)共享实现码。 条款45: 运用成员函数模板接受所有兼容类型真实指针做的很好的一件事情是,支持隐式转换(implicit conversion)。Derived class指针可以隐式转换为base class指针,“指向non-const对象”的指针可以转换为“指向const对象”
但是,同一template的不同具现体之间并不存在什么与生俱来的固有关系。 一个很重要的观察结果是:我们永远无法写出我们需要的所有构造函数。似乎我们需要的不是为SmartPtr写一个构造函数,而是为它写一个构造模板。这样的模板(template)是所谓的member function template,其作用就是为class生成函数。
这一类构造函数根据对象U创建对象T,而U和V是同一个template的不同具现体,有时我们成为泛化generalized copy构造函数。 上述的泛化copy构造函数并未被声明为explicit。那是蓄意的,因为原始指针类型之间的类型转换(例如derived class指针转换为base class指针)是隐式转换,无需显式写出转型动作cast,所以让智能指针仿效这种行径也属合理。但是现实中并没有将int*转化为double*的对于隐式转换行为,所以我们必须对某方面的这一member template多创建的成员函数进行挑选或筛除。
这个行为只有当“存在某个隐式转换可将一个U指针转换为T指针”时才能通过编译,而那正是我们想要的。 member function template的效用不限于构造函数,它们常扮演的另一个角色是支持赋值操作。 在class内声明一个泛化copy构造函数,并不会阻止编译器生成它们自己的copy构造函数,所以如果你想要控制copy构造的方方面面,你必须同时声明泛化copy构造函数和正常的copy构造函数。相同规则也适用于赋值操作。 请记住;请使用member function template生成可接受所有兼容类型的函数; 如果你声明member template用于泛化copy或泛化赋值操作,你还是需要声明正常的copy构造函数和copy赋值操作。 条款46: 需要类型转换时请为模板定义非成员函数
此时编译器不知道我们想要调用哪个函数,它们试图想出什么函数被名为operator*的template具现化出来,它们知道它们应该可以具现化某个名为operator*并接受两个Rational<T>参数的函数,但为完成这一具现化行动,必须先算出T是什么,问题是它们没有这个能耐。因为template实参推导过程从不将隐式类型转换函数纳入考虑。绝不!这样的转换在函数调用过程的确被使用,但在能够调用一个函数之前,首先必须知道那个函数存在。而为了知道它,必须先为相关的function template推导出参数类型(然后才能将适当的函数具现化出来)。 我们需要利用一个事实,就可以缓和编译器在template实参推导方面受到的挑战:template class内friend声明式科研指涉某个特定函数。class template并不依赖template实参推导(后者只施行于function template身上),所以编译器总是能够在class Rational<T>具现化时知道T。
现在operator*的混合式调用就可以通过编译了,因为当对象oneHalf被声明为一个Rational<T>,class Rational<int>于是被局限出来,而作为过程的一部分,friend函数operator(接受Rational<T>)也被自动声明出来。后者身为一个函数而非函数模板(function template),因此编译器可在它调用时使用隐式转换函数(例如Rational的non-explicit构造函数),这便是混合式调用之所以成功的原因。但是却通不过链接。 在一个class template内,template名称可被用来作为template和其参数的简略表达式,所以再Rational<T>内我们只写Rational而不必写Rational<T>。 目前编译器知道我们要调用那个函数(就是接受一个Rational<T>以及又一个Rational<T>),但是函数只被声明在Rational内,并没有被定义出来。我们意图令此class外部的operator* template提供定义式,但是行不通---如果我们自己声明了一个函数,就有责任定义那个函数,既然我们没有提供定义式,连接器当然找不到它。 或许最简单可行的办法就是将operator*函数本体合并在其声明式内。万岁,目前可以运行起来了。
但是这个技术的趣味点在于,我们使用了friend,却与friend的传统用途“访问class的non-public成分”毫不相干。为了让类型转换发生于所有实参身上,我们需要一个non-member函数,为了令这个函数被自动具现化,我们需要将其声明在class内部。 另一种做法是,定义于class内的函数都暗自成为inline函数,包括friend函数。你可以将这样的inline声明所带来的冲击最小化,做法是零operator*不做任何事情,只调用一个定义于class外部的辅助函数。对于更复杂的函数而言,这样做也许就有价值。
许多编译器会强迫你讲所有template定义式放在头文件中,所以你或许需要在头文件内定义doMultiply函数。 请记住:当我们编写一个class template,而它所提供值“与此template相关的”函数支持“所有参数值隐式类型转换时”,请将这些函数定义为class template内部的friend函数。 条款47: 请使用traits classes表现类型信息STL共有5种迭代器分类,对应于它们支持的操作。Input迭代器只能向前移动,一次一步,客户只读取(不能修改)它们所指的东西,而且只能读取一次。 outPut迭代器类似,它们只能向前移动,一次一步,客户只可涂写它们所指东西,而且只能涂写一次。 另一个比较强大的分类是forward迭代器。这种迭代器可以做到前述两种分类做的每一件事,而且可以读写其所指物一次以上。比如单向链表,TR1 hased容器。 Bidirectional迭代器更强,它除了可以向前移动,还可以向后移动。STL的list迭代器就属于这一分类。set,map和unorder版本即是。 最强的当属random access迭代器,因为他可以执行“迭代器算术”,也就是它可以在常量时间内向前或向后跳跃任意距离。内置指针即为。vector,deque,string提供的迭代器也是。 对于这5种分类,C++标准程序库分别提供专属的卷标结构(tag struct)加以确认。
现在回到advance函数,
此时需要判断iter是否为random access迭代器。我们可以用trait在编译期间取得某些信息。 Traits并不是C++关键字预先定义好的构件:它们是一个技术,也是一个C++程序员共同遵守的协议。其要求之一是,它对内置类型和用户自定义类型的表现一样好。 traits必须能施行于内置类型,意味着类型内的嵌套信息这种东西出局了,因为我们无法将信息嵌套与原始指针内。标准技术是把它放入一个template及一个或多个特化版本。
iterator_traits的运作方式是,针对每一个类型IterT,在struct iterator_traits<IterT>内一定声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。 Iterator_traits以两个部分实现上述所言。首先它要求每一个用户自定义的迭代器类型必须嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构。一个针对deque、list迭代器而设计的class看起来是:
iterator_traits的第二个部分如下,专门用来对付指针。为了支持指针迭代器,iterator_traits特别针对指针类型提供一个偏特化版本(partial template specification)。由于指针的行径与random access迭代器类似,所以iterator_traits为指针指定的迭代器类型是:
如何设计并实现一个traits class: ? ? ? ? 确认若干你希望将来取得的类型相关信息; ? ? ? ? 为该信息选择一个名称,例如iterator_category; ? ? ? ? 提供一个template和一组特化版本,内含你希望支持的类型相关信息。 有了iterator_traits,我们可以对之前advance实现伪代码;
虽然它会导致编译问题。等会处理。为什么将可在编译器完成的事延到运行期才做呢? 这不仅浪费时间,也造成可执行文件膨胀。如果我们真的想要一个条件语句(if之类),我们可以用函数重载,其中一个参数为迭代器类型。 现在我们可以总结如何使用一个traits class了; ? ? ? ? 建立一组重载函数(身份像劳工)或函数模板(例如doAdvance),彼此之间的差异在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。 ? ? ? ? 建立一个控制函数(身份像工头)或函数模板(例如advance),它调用上述那些劳工函数,并传递traits class所提供的信息。 请记住:Traits class使得“类型相关信息”在编译器可用。它们以template和template 特化完成实现; 整合重载技术后,traits class有可能在编译器对类型执行if..else测试。 条款48: 认识template元编程Template metaprogramming(TMP,模板元编程)是编写template based C++程序并执行于编译器的过程。 TMP有两个伟大效力。第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至是不可能的。第二,由于template metaprogramming执行于C++编译器,因此可将工作从运行期转移到编译器。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译器找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期转移到编译器的另一个结果是:编译时间变长了。是的,程序如果使用TMP,其编译时间可能远长与不使用TMP的对应版本。
条款47指出,这个typeid-based解法的效率比traits解法低,因为在此方案中, ????????(1)类型检测发生在运行期而非编译期; ? ? ? ? (2)运行期类型测试代码会出现在(或说被链接于)可执行文件中; 我们也在条款47说出,advance的typeid-based实现方式可能导致编译期问题,如下:
因为list的迭代器是个bidirection迭代器,不支持随机读取。尽管编测试typeid的哪一行总是会因为list<int>::iterators而失败,但编译器必须确保所有源码都有效,纵使是不会执行的代码!而当iter不是random_access时,iter+=d无效。与此对比的是traits-based TMP解法,其针对不同类型而进行的代码,被拆分为不同的函数,每个函数所使用的操作(操作符)都可实行于该函数所对付的类型。 TMP已被证明是个图灵完全(Turing-complete)机器,意思是它的威力达到足以计算任何事物。使用TMP你可以声明变量、执行训话、编写及调用函数... 例如TMP如何实现循环。TMP并没有真正的循环构建,所以循环效果是有递归完成的。TMP主要是个函数式语言,而递归之余=于这类语言很重要。TMP的递归甚至是不同正常种类,因为TMP循环并不涉及递归函数调用,而是涉及“递归模板具现化”。例如阶乘的实现:
循环发生在Template具现体Fractorial<n>内部指涉另一个template具现体Factorial<n-1>之时。和所有良好的递归一样,我们需要一个特殊情况造成递归结果。这里的特殊情况就是template特化体Fractorial<0>。 为领悟TMP,很重要一点是先对它能够达成什么目标有一个比较好的理解,下面举3个例子: ? ? ? ? 确保量度单位正确; ? ? ? ? 优化矩阵运算; ? ? ? ? 可以生成客户定制的设计模式; 由于TMP是一个在相对较短时间之前才意外发现的语言,其编程方式还多少依赖经验。尽管如此,将工作从运行期移往至编译期所带来的效率改善还是令人印象深,而TMP对“难以或甚至不可能于运行期实现出来的行为”的表现能力也很吸引人。 请记住: Template metaprogramming可将工作由运行期移至编译期,因而得以实现早期错误侦查和更高的执行效率。 TMP可被用来生成“基于政策选择组合(based on combiations of policy choice)”客户定制代码 ,也可以避免生成对某些特殊类型并不适合的代码。 |
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 21:38:14- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |