1 #author Todd Veldhuizen,石磊
4 #title C++科学计算技巧( Techniques of scientic C++)
6 * <lisp>(setq Title "C++科学计算技巧")</lisp>
7 - 作者:[[mailto:tveldhui@acm.org][ Todd Veldhuizen]]
8 - 翻译: [[http://stoneszone.net][石磊]]
9 - 出处: Indiana University Computer science Technical Report #542 Version 0.4,August 2000
16 所有的C++编译器仅支持ISO/ANSI C++标准的不同子集。试图提供可以运行在不同平台上的C++库或程序是一个让人头疼的事情。
22 如果你需要写可以在不同平台下工作的代码,大部分时间你会在煎熬中度过。在Blitz++库中,我采用了扩展的预处理Kludges来解决不同的编译器。例如下面这段代码:
28 double pow(double x,double y)
29 {return std::pow(x,y);}
33 这段简单的代码在Blitz namespace(名字空间)中定义了一个函数pow,它调用了标准的pow函数。一些编译器不支持<cmath>头文件,所以需要替换为<math.h>。一些编译器支持,但是C math函数不在std的namespace中。甚至一些编译器根本就不支持namespace。我使用的解决方案如下:
36 #ifdef BZ_MATH_FN_IN_NAMESPACE_STD
43 double pow(double x,double y)
45 return BZ_MATHFN_SCOPE(pow)(x,y);
50 这种解决方法很难看。不幸的是,这种情形仍会持续很多年,直到所有的编译器都满足标准。
52 Blitz++自带有一个脚本(bzconfig),它会运行许多小的测试程序来侦测出当前的编译器需要什么样的kludges。[[mailto::Luc.Maisonobe@cnes.fr][Luc Maisonobe]]已经将这些程序加入了Autoconf工具中。你也可以修改Blitz++中的bzconfig脚本来使用它。
54 另一种方法来避免多个平台支持的方法时仅支持gcc编译器。因为它在支持标准方面要好过很多编译器,并且在标准库实现上有较高的提升。可是它在优化上没有KAI c++编译器和一些特定厂商的编译器强。但对于大型的out-of-cache程序,gcc做的很好。(Out-out-cache 程序对于优化不敏感,因为大多数的cpu时间用在了内存上(Stalling for memory).
57 多数的c++编译器使用由EDG(Edison Design Group)写的前端。这个前端是一个很接近标准,健壮可靠,可以提供很有意义的错误信息。你可以在任何平台上找到EDG的身影。其中的一些是:SGI C++,KAI C++,DEC cxx,Gray C++,Intel C++,Portland Group C++,Tera C++。
59 大部分的c++编译器(Borland/Inprise,Microsoft,etc)不符合标准的程度令人惊讶。它们都关注在COM,DCOM,WFC等上。一点都不关心标准的支持。Microsoft's 6.0 C++编译器在编译简单的成员模版时都会经常的崩溃。在和Microsoft开发人员对话的过程中,他们说这是因为他们的客户不会关心是否符合标准,而是在关心一些WFC/COM等东西。也许这是对的。但是如果你在自己最喜欢的编译器上由于标准问题而受挫,那就因该向开发商投诉,否则他们是不会考虑这些的。
63 大部分的编译器在优化C++程序上令人失望。对于在c++中经常出现的嵌套的聚集和零时变量,常规的优化技巧需要修改。但是KAI编译器是一个例外,它的优化做的很好。另外一个是SGI编译器(虽然对于KAI,目前在零时变量消除上还做的不够好)
65 KAI在优化技术上有美国专利,导致其他厂商不能实现相同的优化技术。尽管如此,它还是给EPC++编译器提供了优化引擎的使用许可证。
68 对Blitz++抱怨最多的是编译时间过长(以及其他的template C++ 库)。下面小节将介绍为何C++程序需要这么长的时间编译。
71 头文件是一个大问题。C++没有一个很有效的模块系统。导致大量的头文件在每一个编译模块都需要被解析和分析。例如, 包括<iostream>在有些编译器上会拉入成千上万行的代码。
73 Template 库的情况更加严重。因为所有的模版聚现都在编译期进行。所以除了函数声明,头文件还包括了函数定义。
75 例如,在我的桌面工作站用gcc编译Blitz++的 example/array.cpp例子需要37秒。如果你除去其中所有的代码,只留下#include<blitz/array.h>这一行,编译竟然还需要22秒。也就是是说,60%的时间都被用来编译头文件了!
77 预先编译头文件技术可以在一定程度上缓解这个问题,但是有时候他们也会让问题更加严重。在Blitz++中开启Precompiled Header可以产生大约64Mb的dumps,读取它会比它取代的原头文件华更多的时间。
79 本节告诉你要尽量保持你的Header越小越好。当开发一个库时,提供很多个小的头文件来在相应的情况下分别包含,而不应该仅仅提供一个包含所有程序库的超大头文件。
82 模版给编译器导致了一个严重的问题。什么时候他们才需要被聚现化?这通常有两种方式:
84 - 对于每一个编译模块,声称所有需要的模版实例。这导致多重模版实例-如果foo1.cpp和foo2.cpp都使用了 list<int>, 你得到了两个list<int>的实例-一个在foo1.o中,另一个在foo2.o中。一些连接器会自动的抛弃重复的实例。
85 - 不做任何聚现化。而在连接时期来确定什么模版需要聚现,然后回到编译去生成它们。这就叫做Prelinking
87 不幸的是prelinking需要prelinking你的整个程序很多次。
89 一个模块被重复编译三次很常见。你可以想象,对于一个大型的程序这将耗费多少编译时间。
91 采用EDG前端的编译器可以强制不做prelinking,通过option -tused 或者-ptused实现。
93 **** 1.2.3 程序数据库方法-visual age c++
95 IBM的Visual Age C++ 编译器对于分别编译有一个有趣的技术。它把所有的代码保存在一个数据库中。由于完整的源代码都是可以在编译期间取得的(而不是仅仅一个编译单元的代码),VAC++ 不需要做prelinking或者担心分别编译问题。编译器做增量编译,但是仅针对代码改变的情况。这种技术可以大幅度的降低编译template c++ 库的时间。
99 像Blitz++的库使用了大量的template。一些编译器在模版聚现上有自己的算法,但适应性不是很好。例如下面的代码:
114 如果聚现foo<N>,那么foo<N-1>被聚现,同样foo<N-1>,...,直到foo<0>。编译需要多少时间?聚现的函数个数是N个,所以编译时间应该随N线性增长,对吗?
116 不幸的是对一些编译器并不是这样。简单的说,许多编译器自身保存一个链表,确定一个template是否被聚现过需要线性搜索。所以编译时间为O(N^2).这对于很少的template的程序不成问题,但对于像Blitz++那样有上千个template需要聚现的程序库就很致命。
118 同样的问题可以发生在优化算法上。比如别名检查。我在一些主流C++编译器上发现了O(N^2),O(N^3)的优化算法。
122 在C++的世界里,多态意味着虚函数。这种多态是很有作用的设计方法。但是使用虚函数会有大的性能损失。
125 - 虚函数分派可以导致不可预料的执行分支-流水线作业会停止(在一些平台上游branch cache,可以避免这种问题)
126 - 目前的C++ 编译器不能优化虚函数调用: 它阻止了指令scheduling,数据流分析,循环展开等。
128 目前一些研究项目已经成功运用了替换虚函数的方法,称为直接分发(direct dispatch)。但是这种方法还没有在任何编译器上实现。一个障碍是去除虚拟函数分派需要对全部程序进行分析-你必须有整个程序才能在编译时间分析。几乎所有的C++实现都对编译模块进行分别的单独编译-唯一的例外是Visual Age C++。
130 必须了解的是,尽管如此,虚函数并不是在任何情况对于性能都是死亡之吻。最重要的是:有多少代码在虚函数中?如果有很多的代码,则虚函数对性能的影响不会很可观。如果很少的话,则影响会很严重。
134 - 在虚函数中的代码很少(e.g 小于25flops)。
135 - 虚函数使用的很频繁(e.g. 在循环中)。你可以使用profiler来判断这点。
137 虚函数在函数比较大(进行很多的计算)的时候高度推荐使用,或者不是调用很频繁的话。
139 不幸的是,在科学计算程序中,一些对于虚函数最有用处的地方都在循环中,并且一般都是小函数。一个比较经典的例子如下:
143 public:virtual double operator()(int i,int j)=0;
146 class SymmetricMatrix:public Matrix{
147 public:virtual double operator()(int i ,int j);
150 class UpperTriangularMatrix:public Matrix{
151 public:virtual double operator()(int i,int j);
155 虚函数分派operator()来从矩阵中读取元素,这会破坏任何一种矩阵算法的性能。
157 **** 1.3.2 解决方法A: 简单引擎(simple engines)
158 "engines"来自在LANL的POOMA团队。设计模式社区也有一个同样的名称。
160 想法是把动态多态(虚函数)替换为静态多态(template parameters)。这是一个采用了这种技术的Matrix的骨架。
167 class UpperTriangular{
171 tempalte<class T_engine>
178 template<class T_engine>
179 double sum(Matrix<T_engine>& A);
187 在上例中,矩阵结构的不同变化隐藏在engines Symmetric和UpperTriangular中。Matrix类把engine作为template parameter,并且包含这个engine的实例作为成员变量。
189 用户使用的符号变了:Matrix<Symmetric> 而不是原来的SymmetricMatrix,不过这个不是一个大问题。我们可以使用typedefine来隐藏这个细节。同样,在Matrix中,大部分操作要被代理给engines来处理。
191 这种解决方法最严重的问题是所有的Matrix子类型都必须有同样的成员函数。例如,如果我们想要Matrix<Symmetric>有一个isSymmetricPositiveDefinite()的方法,典型的实现是:
194 template<class T_engine>
197 //Delegate to the engine
198 bool isSymmetricPositiveDefinite()
199 {return engine.isSymmetricPositiveDefinite();}
207 bool isSymmetricPositiveDefinite()
214 但是这并没有使Matrix<UpperTriangular>有理由包含isSymmetricPositiveDefinite()函数,因为upper triangular矩阵不能为对称。从上例可以看出这种方法使基类包含了所有子类型所拥有的函数。所以导致产生了很多在engine中会抛出异常的成员函数。
218 class UpperTriangular{
220 bool isSymmetricPositiveDefinite()
222 throw makeError("Method isSymmetricPositiveDefinite() is not defined for UpperTriangular矩阵");
230 另一种方法:忽略这些函数定义会把你的用户推进火坑。因为当用户试图使用这些未定义的函数的时候,会得到一大堆奇怪的模版聚现错误。
234 **** 1.3.3 解决方案B: Barton 和Nackman 技术
235 这种技术称为"Barton and Nackman Trick",因为 他们在他们的杰出著作<<Scientific and Engineering C++>>中使用了这种技术。Geoff取了一个比较有意义的名字"Curiously Recursive Template Pattern"。
237 如果你没有见过这种技巧,那么它一定会让你开始抓头的。是的,它是合法的。
241 //基类有一个模版参数。这个参数定义了该类应该从哪里继承。
242 template<class T_leaftype>
246 {return static_cast<T_leaftype&>(*this);}
248 double operator()(int i,int j)
249 {return asLeaf()(i,j);} //代理给leaf
253 class SymmetricMatirx : public Matrix<SymmetricMatrix>{
257 class UpperTriMatrix: public Matrix<UpperTriMatrix>{
262 template<class T_leaftype>
263 double sum(Matrix<T_leaftype>& A);
270 这种技术可以实现一般化的继承体系结构。关键点在于基类有一个模版参数,该参数为后代(叶子)类的类型。保证了在编译期间该类型信息是可以得到的-不再需要虚函数的分派。
272 在继承类中可以任意选择性的进行定制(例如,基类可以实现默认功能,继承类重载相应的功能)
274 与上节解决方法不同,继承类可以有其特殊的,仅属于自己的(基类没有定义)成员函数。
276 这种方法,基类仍然要将一切调用代理给继承类。
278 使你的继承树多于一层是可行的,但需要多加考虑。
280 *** 1.4 Callback inlining 技术
281 Callback 经常会出现在程序中。一些典型的例子如下
282 - 数值积分:一个一般性的积分程序使用Callback函数来对需要被积分的函数求值。在C库中,通过传入函数指针的参数实现。
285 - 排序函数,如qsort()和bsearch()需要callback来比较变量。
287 标准的C函数方法采用函数指针来实现Callback。
289 double integrate(double a,double b,int numSamplePoints,double(*f)(double){
290 double delta=(b-a)/numSamplePoints-1);
293 for(int i=0;i<numSamplePoints;++i)
296 return sum*(b-a)/numSamplePoints;
300 这种方法工作的很好,但是对性能有影响:在内部循环中的函数调用会导致较大的性能损失。一些优化编译器也许会执行inter-procedural和指针分析来确定f指向的确切函数,但是大多数编译器不会做。
302 **** 1.4.1 Callbacks: C++方式
303 在C++库中,callbacks一般采用虚函数。新一点的方式是定义一个基类,如Ietegrator,定义一个虚函数,在继承子类中定义实际需要被积分的函数。
309 double integrate(double a,double b,int numSamplePoints)
311 double delta=(b-a)/..;
314 for(int i=0;i<numSamplePoints;++i)
315 sum+=functionToIntegrate(a+i*delta);
316 reutrn sum*(b-a)/numSamplePoints;
319 virtual double functionToIntegrate(double x)=0;
322 class IntegrateMyFunction:Integrator{
324 virtual double functionToIntegrate(double x)
331 同C类似,虚函数分派在内部循环会导致性能下降(如果该函数执行时间比较长,则虚函数的损失可以忽略不计)。对于性能,需要在积分函数中采用inline the callback函数。
333 下面就介绍三种方法实现inlined callbacks。
335 1. Expression templates
337 这是一种精细,复杂的解决方法。它扩展和维护起来比较难。一般不必浪费你的精力去选择这种方法。
341 Callback函数可以被封装在一个类中,从而创建一个函数对象或者叫仿函数。
349 double operator()(double x)
350 {return 1.0/(1.0+x);}
353 template<class T_function>
354 double integrate(double a,double b,int numSamplePoints,T_function f)
356 double delta=(b-a)/(numSamplePoints-a);
359 for(int i=0;i<numSamplePoints;++i)
362 return sum*(b-a)/numSamplePoints;
367 integrate(0.3,0.5,30,MyFunction());
371 一个MyFunction的对象作为参数传入integrate函数。C++编译器会聚现化integrate, 用MyFunction来替换T_function;这样会导致MyFunction::operator()被inlined进integrate()的内部循环中。
373 这种方法的缺点是用户必须将所有他想调用的函数封装在类中。同样,如果Callback函数需要参数,则必须提供给MyFunction的构造函数,并保存为成员变量。
375 注意,你可以向上例的integrate传入函数指针。但这和C风格的callback函数一样,不会在循环中将你传入的函数指针进行inline化。
382 double function1(double x)
387 template<double T_function(double)>
388 double integrate(double a,double b,int numSamplePoints)
391 double delta=(b-a)/(numSamplePoints-1);
394 for(int i=0;i<numSamplePoints;++i)
395 sum+=T_function(a+i*delta);
397 return sum*(b-a)/numSamplePoints;
402 integrate<function1>(1.0,2.0,100);
405 integrate()函数将指向callback函数的函数指针作为模版参数。由于这个指针在编译期间就可以确定,所以function1被inline进入内部循环。参考oon-list档案的Sumclass algorithms条目。
408 代码膨胀对于基于模版的库来说是一个问题。例如,你使用queue的一些实例:
416 你的程序会有三个不同版本的queue<>。这对于使用基于虚函数多态的老风格c++库并不是问题。
418 但是,实际中编译器会生成一些荒谬的多余代码使得问题更加严重。
419 - 一些编译器在每个模块中聚现模版,并且在连接时报重复对象错误。
420 - 一些编译器(老的)聚现模版的每一个成员函数。ISO/ANSI的编译器禁止这中做法:编译器应该只聚现真正实用的模版。
422 这些问题一部分是我们自己引起的: 使用模版生成代码太容易了,以至于程序员很少考虑它的后果。我趋向于在任何地方使用模版,很多时候更不不去考虑使用虚函数。我个人一般不关心代码膨胀,因为我的程序一般都很小。但是工作在大型程序的人必须考虑使用模版还是虚函数。
424 - 虽然模版可以带来较好的性能,但是也意味着大多数情况带来较多的代码。(如果你只用模版其中的一个实例,他们不会给你更多的代码)
425 - 虚函数给你最少的代码,但是却是较慢的代码。这种性能损失在虚函数函数计算量较大的时候是可以忽略的,或者在函数调用较少的时候。
427 当然,虚函数和模版并不是对每个问题都是可以选择的。如果你需要动态的多态,模版就不能胜任。下面就给出一些降低代码膨胀的建议。
429 **** 1.5.1 避免kitchen-sink模版参数
430 将类的每个属性都作为模版参数是很有诱惑力的。下面就是一个夸张的例子。
433 template<typename number_type,int N,intM,bool sparse,typename allocator,bool symmetric,bool banded,int bandwidthIfBanded>
437 每个多余的模版参数都为代码膨胀提供了机会。如果你使用了很多种"kitchen-sink"的模版类,你应该考虑通过虚函数,成员模版等减少一些模版参数。
439 例如,我们有一个类list<>。我们想要用户有能力提供自己的内存分配器,所以我们可以将Alloctor作为一个模板参数:
442 list<double ,allocator<double>> A;
443 list<double , my_alloctor<double>>B;
446 这样会为A和B生成不同的list版本,尽管两者大部分都是一模一样的(比如你要遍历list,你就不需要考虑它在内存中是怎么分配的)。采用alloactor作为模版参数有什么好处吗?几乎没有-内存分配是很慢的,所以附加的虚函数损失对他来说是可以接受的。下面的方法可以获得同样灵活的设计:
449 list<double> A(allocator<double>());
450 list<double> B(my_allocator<double>());
453 其中allocator和my_allocator是多态的。
456 你可以通过将函数定义移动至类定义的外面来避免创建不需要的重复成员函数。例如,下面是一个Array3D类的框架。
459 template<class T_numtype>
463 template<class T_numtype2>
464 bool shapeCheck(Array3D<T_numtype2>&);
467 int base[3]; //起始x,y,z的index值
468 int extent[3]; //每个x,y,z的长度
471 template<class T_numtype> template<class T_numtype2>
472 bool Array3D<T_numtype>::shapeCheck(Array3D<T_numtype2>&B)
475 if((base[i]!=B.base(i))||(extent[i]!=B.extent(i)))
484 shapeCheck()函数确定两个array是否有同样的起始index和长度。如果你有很多种不同的array(Array3D<float>,Array3D<int>,...),你会最终拥有很多个shapeCheck()函数的实例。但是,其实这些实例是完全一样的,因为shapeCheck()并不使用参数T_numtype和T_numtype2。
486 为了避免产生多重的实例,你可以将shapeCheck()移动至没有模版参数的基类,或者移至全局函数,如下:
492 bool shapeCheck(Array3D_base&);
500 template<class T_numtype>
501 class Array3D:public Array3D_base{
506 由于shapeCheck()现在不在模版类中,所以只有一个它的实例。如果你正好是C++编译器的作者,读到这里,你应该注意,这个问题可以通过自动的进行模版参数有效性检测来避免。
508 **** 1.5.3 Inlining 级别
509 一般情况,我趋向于在Blitz++中将很多东西inline化,因为这样子会协助编译器优化(对于那些不做interprocedural分析和强化inlining的编译器)。如果有一天,我感觉Blitz++中的inline太多了,这有一种策略可以处理这个问题。
512 #if BZ_INLINING_LEVEL==0
516 #elif BZ_INLINING_LEVEL==1
517 #define _bz_inline1 inline
519 #elif BZ_INLINING_LEVEL==2
520 #define _bz_inline1 inline
521 #define _bz_iiline2 inline
524 _bz_inline void foo(Array<int>&A)
530 用户如果不想要任何的inline,可以-DBZ_INLINING_LEVEL=0来编译,还可以控制其为1,2来控制INLINE的程度。
533 容器类可以大致分为两个部分:STL风格(基于iterator的)和Data/View风格的。
536 - 子容器通过iterator range来指定,一般采用开放-封闭的区域:[iter1,iter2)。
537 - 这种设计风格来自STL;如果你通过这种方法写容器,你可以取得无缝调用STL算法的优势。
538 - 容器至少需要有1-D序列的语义。所有的子容器必须intervals进那个1D的顺序。这会对于多维数组会产生问题。(例如,A(2:6:2,2:6:2)的元素明显不具有1-D序列的语义)
539 - iterator基于指针的概念,而指针又是C++中最容易犯错误的地方之一。这导致很多容易错误的使用iterator。
540 - 对于函数来说,将容器作为值来返回比较困难。相反,容器可以用传入参数的方法(argument)来接受结果内容。
542 **** 1.6.2 Data/View 容器
543 - 容器的内容(数据)保存在容器外部。容器只是一个数据的轻量级句柄。
544 - 多个容器对象可以索引同一个数据内容,但提供它的不同的视图。
545 - 使用参考计数:容器的数据内容在最后一个句柄销毁后销毁。这提供了一个垃圾回收机制的实现(且有效率)。
546 - 有些情况下定义子容器比较容易(例如,多维数组的子数组)。
547 - 子容器可以是第一级别(first class)的容器(例如当你取得Array<N>的子数组,结果仍然是Array<N>,而不是SubArray<N>)
548 - 使用引用计数的容器需要较为精细的设计方法。
549 - 从函数中返回容器比较容易,并且可以将容器按值传递。
553 Aliasing对C++科学计算库来说是影响性能的一大问题。例如,考虑计算1阶矩阵A <- A+xxT:
556 void rank1Update(Matrix&A,const Vector&x)
558 for(int i=0;i<A.rows();++i)
559 for(int j=0;j<A.cols();++j)
564 对于好的性能,编译器需要保持x(i)在寄存器中而不是在内部循环里重复的载入它。尽管如此,编译器不能确定的是在A中的数据不会覆盖x(i)中的数据。这对于矩阵和向量类的设计来说是不可能采用的,但是编译器不知道这些。Aliasing的可能性导致编译器产生低效率的代码。
566 另一个Aliasing的结果是中断循环的软件流水线。 例如,一个DAXPY操作y <- y + a*x
569 void daxpy(int n,double a,double*x,double *y)
577 为了取得好的效率,一个优化编译器会部分的展开循环,然后进行软件流水线来找到有效的内部循环方法。设计这个方法经常需要改变循环中的载入和存储顺序,例如:
585 double t0=yt[0]+a*xt[0];
586 double t1=yt[1]+a*xt[1];
588 double t2=yt[2]+a*xt[2];
589 double t3=yt[3]+a*xt[3];
591 double t3=yt[3]+a*xt[3];
600 不幸的是,编译器无法改变载入和存储的顺序因为x,y指针可能在内存中重迭。在Fortran77/90中,编译器可以假设数组地址没有重叠。对于在cache中的数组这会导致20-50%的性能差异。这依赖于特定的循环/架构。Aliasing对于out-of-cache数组来说不是大问题。
602 优秀的c/c++编译器会做Aliasing分析,目的是确定是否可以认为指针可被认为是指向不重叠内存的。Alias分析可以减少许多的Aliasing可能性,但是他不是完美的。例如,分析像daxpy函数的程序必须做全局分析。编译器必须在同一时间考虑你执行的所有函数。除非你的编译器做了全局alias分析(SGI会),否则你就会有aliasing问题。
604 NCEG(Numerical C Extensions Group)设计了一个关键字restrict,可以被一些编译器识别(KAI,Cray,SGi,Intel,gcc)。restrict是一个可以告诉编译器该变量没有Aliasing的修饰符。例如:
607 double * restrict a; //a[0],a[1]..没有aliasing
608 int & restrict b ; //b没有aliases
611 在C++中restrict的明确定义并不是很nailed down。但是当restrict使用后,编译器可以自由的优化而不去考虑装载/存储顺序。
613 不幸的是将restrict加入iso/ansi c++标准的提议被否决了。我们最终得到的是一个能力有争议的valarray。valarray数组类设计为没有aliasing,因此可以允许编译器对其上的操作进行优化。
615 尽管如此,restrict已经被ansi/iso c列为标准,因此有可能在下一次中加入c++标准。当可以使用restrict时,开发程序可以使用预先编译kludge。例如,blitz++定义了如下的宏:
618 #ifdef BZ_HAS_RESTRICT
619 #define _bz_restrict restrict
626 double * _bz_restrict data_;
631 在一些平台上,编译器有选项可以允许编译器假设没有aliasing。这可以导致好的效果,但是必须给予注意。这些选项会改变你所有程序中的指针语意义semantics of pointers。如果你对指针做了写特殊处理,这可能会导致问题。
634 Traits技术是由Nathan Myers在C++ Report 1995年June发表的[[http://www.cantrip.org/traits.html][《Traits:a new and useful template technique》]]中最新提出的。
635 Trait类保存一些映射关系,将一些概念映射到另一些上去。你可以从以下东西发出映射:
641 并且可以映射到几乎所有你想要的(类型,常数,运行期间变量,函数)。
643 **** 1.8.1 例子: average()
648 T average(const*T data,int numElements)
651 for (int i=0;i<numElements;++i)
653 return sum/numElements;
658 如果T是一个浮点数,它会工作的很好,但是如果是一下的情况,就不好说了:
659 - T是一个整数(int,long):计算的平均值就会被截断并按照整数来返回