为什么C ++ STL如此大量地基于模板? (而不是*接口*)

我的意思是,除了它的义务名称(标准模板库)…

C ++最初是想将OOP概念提供给C语言。那就是:你可以根据类和类的层次结构来说明一个特定的实体可以做什么(不pipe它是怎么做的)。 由于多重inheritance的问题,一些能力组合更难以描述,而且C ++支持接口的概念有点笨拙(与java等相比),但它在那里(可能是改善)。

然后模板和STL一起发挥作用。 STL似乎采用了传统的OOP概念,并使用模板来代替它们。

应该区分使用模板来概括types的情况,其中types本身与模板的操作无关(例如容器)。 有一个vector<int>是非常有意义的。

然而,在许多其他情况下(迭代器和algorithm),模板化types应该遵循一个“概念”(Input Iterator,Forward Iterator等),其中概念的实际细节完全由模板的实现来定义函数/类,而不是与模板一起使用的类,这是OOP的一些反用法。

例如,你可以告诉这个函数:

 void MyFunc(ForwardIterator<...> *I); 

更新:由于在原来的问题中还不清楚,ForwardIterator可以自行模板化,以允许任何ForwardIteratortypes。 相反,将ForwardIterator作为一个概念。

希望只有通过查看其定义才能获得Forward Iterator,在此您需要查看以下实现或文档:

 template <typename Type> void MyFunc(Type *I); 

我可以赞成使用模板的两个说法:通过为每个使用的types定制编译模板,而不是使用vtables,可以使编译代码更高效。 事实上,模板可以用于本机types。

然而,我正在寻找更为深刻的理由,为什么放弃古典OOP而赞成STL的模板化? (假设你读了那么多:P)

简短的回答是“因为C ++已经移动”。 是的,回到70年代后期,Stroustrup打算创build一个升级的具有OOPfunction的C,但这是很久以前的事了。 当语言在1998年被标准化时,它不再是OOP语言。 这是一个多范式的语言。 它当然有一些OOP代码的支持,但是它也有一个图灵完成的模板语言覆盖,允许编译时元编程,人们已经发现了generics编程。 突然之间,OOP似乎并不重要。 不是当我们可以通过使用模板和generics编程可用的技术来编写更简单,更简洁更高效的代码。

OOP不是圣杯。 这是一个可爱的想法,它是在70年代发明的程序语言相当改进。 但说实话,并不是所有的事情都被破解了。 在许多情况下,它很笨拙和冗长,并没有真正促进可重用的代码或模块化。

这就是为什么C ++社区今天对generics编程更感兴趣,为什么大家终于开始意识到函数式编程也是非常聪明的。 OOP本身并不是一个美丽的景象。

尝试绘制假设的“OOP-ified”STL的依赖关系图。 有多lessclass级需要了解彼此? 会有很多的依赖关系。 你能够只包括vector头,而不会得到iterator ,甚至iostream拉? STL使这个很简单。 一个向量知道它定义的迭代器types,就这些了。 STLalgorithm一无所知 。 他们甚至不需要包含一个迭代器头,尽pipe他们都接受迭代器作为参数。 哪个更模块化呢?

STL可能不像Java定义的那样遵循OOP的规则,但是它不能达到OOP的目标吗? 它不是达到可重用性,低耦合,模块化和封装吗?

而且它是否比OOP版本更好地实现这些目标?

至于为什么STL被纳入语言,导致STL发生了几件事情。

首先,模板被添加到C ++。 他们被添加了非常相似的原因generics被添加到.NET。 能够写出类似“T型容器”的东西而不会丢弃types安全性似乎是一个好主意。 当然,他们落实的执行情况是相当复杂和强大的。

然后人们发现他们添加的模板机制比预期的更强大。 有人开始尝试使用模板来编写更通用的库。 一个受函数式编程的启发,一个使用了C ++的所有新function。

他把它提交给了C ++语言委员会,这个委员会花了相当长的时间来适应它,因为它看起来很奇怪和不同,但是最终意识到它比传统的面向对象的方法更好,否则就不得不包括在内 。 于是他们对它进行了一些调整,并将其纳入标准库。

这不是一个意识形态的select,它不是一个“我们想不想OOP”的政治select,而是一个非常实用的select。 他们对图书馆进行了评估,看到它工作得很好。

无论如何,你提到的赞成STL的原因都是绝对必要的。

C ++标准库必须高效。 如果效率低于相应的手动C代码,那么人们就不会使用它。 这会降低生产力,增加错误的可能性,总体而言,这只是一个坏主意。

而且STL 必须使用原始types,因为原始types是C中所有的,并且是两种语言的主要部分。 如果STL没有与本地数组一起工作,那就没用了

你的问题有一个很强的假设,即面向对象是“最好的”。 我很好奇听到为什么。 你问为什么他们“抛弃古典OOP”。 我想知道为什么他们应该坚持下去。 它有哪些优势?

我认为你所要问/抱怨的最直接的答案是这样的:假设C ++是OOP语言是一个错误的假设。

C ++是一个多范式语言。 它可以使用OOP原理进行编程,它可以程序化编程,可以一般编程(模板),用C ++ 11(以前称为C ++ 0x)甚至可以在function上进行编程。

C ++的devise者认为这是一个优势,所以他们会争辩说,当通用编程更好地解决问题时,将C ++限制为纯粹的面向对象(OOP)语言,并且更一般地说 ,是退步。

我的理解是,Stroustrup最初更喜欢“OOP风格”的容器devise,事实上没有看到任何其他的方式来做到这一点。 Alexander Stepanov是STL的负责人, 他的目标并不包括“使其面向对象” :

这是基本点:algorithm是在代数结构上定义的。 我花了几年时间才意识到,你必须扩大结构的概念,增加复杂性要求到正则公理。 …我相信迭代器理论是计算机科学的核心,因为环的理论或Banach空间是math的核心。 每当我看一个algorithm,我会试图find一个定义它的结构。 所以我想做的是一般性地描述algorithm。 这就是我喜欢做的事情。 我可以花一个月时间研究一个众所周知的algorithm,试图find它的通用表示。 …

至less对于我来说,STL代表编程的唯一可能方式。 实际上,与C ++编程完全不同,它在大多数教科书中都有介绍。 但是,你看,我没有试图用C ++编程,我试图find正确的方式来处理软件。 …

我有很多错误的开始。 例如,我花了数年的时间试图find一些用于inheritance和虚拟的东西,然后才明白为什么这个机制有根本的缺陷,不应该使用。 我很高兴没有人能看到所有的中间步骤 – 其中大部分都非常愚蠢。

(他解释了为什么inheritance和虚拟 – 又名面向对象的devise“在根本上是有缺陷的,不应该在面试的其余部分使用”)。

一旦Stepanov将自己的图书馆提交给Stroustrup,Stroustrup和其他人经历了艰巨的努力,将其纳入ISO C ++标准(同一访谈):

Bjarne Stroustrup的支持至关重要。 Bjarne真的想要STL的标准,如果Bjarne想要的东西,他得到它。 他甚至强迫我改变STL,我永远不会为别人做出任何改变…他是我认识的最单一的人。 他把事情做完了。 他花了一段时间才明白STL是怎么回事,但是当他这样做的时候,他已经准备好了。 他还为STL做出了贡献,他坚持认为不止一种编程方式是有效的 – 十多年来没有结束,而且追求灵活性,效率,超载和types安全的结合使STL成为可能的模板。 我想很清楚地说明,Bjarne是我这一代杰出的语言devise师。

答案在STL的作者斯捷潘诺夫的采访中被发现:

是。 STL不是面向对象的。 我认为面向对象几乎和人工智能一样是一个骗局。 我还没有看到来自这些OO人员的一段有趣的代码。

为什么一个纯数据结构和algorithm库的OOPdevise会更好? 面向对象并不是每一件事的解决scheme。

恕我直言,STL是我见过的最优雅的图书馆:)

对于你的问题,

你不需要运行时多态性,STL实际上使用静态多态来实现库是一个优点,这意味着效率。 尝试编写一个通用的sorting或距离或适用于所有容器的algorithm。 你的Sort in Java会调用通过n级dynamic执行的函数!

你需要像拳击和拆箱这样的愚蠢的东西来隐藏所谓的纯粹的OOP语言的讨厌的假设。

我看到的唯一的问题是STL,一般的模板是糟糕的错误信息。 这将在C ++ 0X中使用Concepts解决。

比较STL和Java中的集合就像比较泰姬陵到我家:)

模板types应该遵循一个“概念”(Input Iterator,Forward Iterator等),其中概念的实际细节完全由模板函数/类的实现来定义,而不是由类的类来定义与模板一起使用,这是OOP的一些反用法。

我想你误解了模板对于概念的使用。 例如,Forward Iterator是一个非常明确的概念。 要查找为了使类成为Forward Iterator而必须有效的expression式,以及它们的语义(包括计算复杂性),请查看标准或http://www.sgi.com/tech/stl/ForwardIterator.html (你必须按照Input,Output和Trivial Iterator的链接来查看)。

该文件是一个非常好的界面,“概念的实际细节”在这里定义。 它们不是由Forward Iterator的实现定义的,也不是由使用Forward Iterator的algorithm定义的。

在STL和Java之间处理接口的差异有三个方面:

1)STL使用对象定义有效expression式,而Java定义了必须在对象上可调用的方法。 当然,一个有效的expression式可能是一个方法(成员函数)调用,但它不一定是。

2)Java接口是运行时对象,而即使使用RTTI,STL概念在运行时也是不可见的。

3)如果你不能使STL概念所需的有效expression式有效,当你用types实例化一些模板时,你会得到一个未指定的编译错误。 如果你没有实现一个Java接口的必要方法,你会得到一个特定的编译错误。

第三部分是如果你喜欢一种(编译时)的“duck typing”:接口可以是隐式的。 在Java中,接口是有些明确的:一个类“是”Iterable当且仅当它说它实现Iterable。 编译器可以检查其方法的签名是否全部存在并且是正确的,但是语义仍然是隐含的(即,它们或者被logging或者不是,但是只有更多的代码(unit testing)可以告诉你实现是否正确)。

在C ++中,就像在Python中一样,语义和语法都是隐含的,尽pipe在C ++中(如果你使用强types预处理器的话,在Python中),你可以从编译器中得到一些帮助。 如果程序员需要实现类的类似Java的显式声明,那么标准的方法是使用types特征(并且多重inheritance可以防止这太冗长)。 与Java相比,缺乏什么是一个单一的模板,我可以用我的types实例化,当且仅当所有必需的expression式对我的types有效时才编译。 这将告诉我是否已经实现了所有必需的位,“在我使用之前”。 这是一个方便,但它不是OOP的核心(它仍然不testing语义,而testing语义的代码自然也会testingexpression式的有效性)。

STL可能会或可能不会满足您的口味,但它确实将界面从执行中分离出来。 它确实缺乏Java对接口进行reflection的能力,它以不同的方式报告接口要求的违反。

你可以告诉该函数…期待一个前向迭代器,只要看看它的定义,你需要看看实现或文档…

就我个人而言,如果使用得当,隐式types是一种优势。 该algorithm说明了它的模板参数,实现者确保这些工作正常工作:这正是“接口”应该做什么的共同点。 而且,对于STL,你不太可能使用std::copy基于在头文件中find它的前向声明的。 程序员应该根据它的文档来制定一个函数的function,而不仅仅是函数签名。 在C ++,Python或Java中都是如此。 在使用任何语言进行input时可以实现的function是有限制的,并且尝试使用键入来做一些不能做的事情(检查语义)将会是一个错误。

也就是说,STLalgorithm通常以一种明确需要什么概念的方式来命名模板参数。 但是,这是在文档的第一行中提供有用的额外信息,而不是使声明更具信息性。 还有更多的东西需要知道,可以封装在参数的types,所以你必须阅读文档。 (例如,在采用input范围和输出迭代器的algorithm中,输出迭代器很有可能需要足够的“空间”来input一定数量的输出,这取决于input范围的大小,也可能是其中的值。 )

这里是关于明确声明的接口的Bjarne: http : //www.artima.com/cppsource/cpp0xP.html

在generics中,参数必须是从generics定义中指定的接口派生的类(C ++等价于接口是抽象类)。 这意味着所有generics参数types都必须适合层次结构。 这对devise施加了不必要的限制,需要开发人员的无理预见。 例如,如果你写了一个generics并且定义了一个类,除非我知道你指定的接口并从中派生了我的类,否则人们不能将我的类作为generics的参数。 这是僵化的。

反过来看,用鸭子打字,你可以实现一个接口,而不知道接口存在。 或者有人可以故意编写一个接口,让你的类实现它,咨询了你的文档,看看他们不要求任何你还没有做的事情。 这是灵活的。

“OOP对我来说意味着只有消息传递,本地保留,保护和隐藏状态过程,以及所有事情的极端绑定。可以在Smalltalk和LISP中完成,也可能有其他系统,但是我不知道他们。“ – AlanTalk,Smalltalk的创造者。

C ++,Java和大多数其他语言都离古典OOP很远。 这就是说,争取意识形态并不是很有成效。 C ++在任何意义上都不是纯粹的,所以它实现了当时似乎具有实用意义的function。

与基本问题

 void MyFunc(ForwardIterator *I); 

你是如何安全地获得迭代器返回的types? 使用模板,这是在编译时为您完成的。

STL首先提供了一个涵盖最常用algorithm的大型图书馆,其目的在于expression行为和performance 。 模板成为实现目标和实现目标的关键因素。

只是提供另一个参考:

Al Stevens在1995年3月的DDJ采访Alex Stepanov:

Stepanov解释了他的工作经验和对大型algorithm库的select,最终演变为STL。

请介绍一下您对generics编程的长期兴趣

…..然后,我在贝尔实验室提供了一个工作在C ++库C ++组的工作。 他们问我是否可以用C ++来做。 当然,我不知道C ++,当然,我说我可以。 但是我不能用C ++做,因为在1987年,C ++没有模板,这对于实现这种编程风格是必不可less的。 inheritance是获得通用性的唯一机制,这是不够的。

即使现在,C ++inheritance对于generics编程也没有多大用处。 让我们来讨论为什么。 许多人试图使用inheritance来实现数据结构和容器类。 正如我们现在所知道的那样,如果有任何成功的尝试,也很less C ++inheritance,以及与之相关的编程风格受到极大的限制。 使用它作为平等的事情来实现一个包括微不足道的事情的devise是不可能的。 如果从层次结构的根目录开始使用基类X,并在此类上定义一个虚拟的相等运算符,该运算符接受Xtypes的参数,则从类X派生类Y.相等的接口是什么? 它将Y和X比较是平等的。以动物为例(OO人喜欢动物),定义哺乳动物并从哺乳动物中获得长颈鹿。 然后定义一个成员函数mate,在那里动物与动物配对并且返回一个动物。 然后,你从动物中得到长颈鹿,当然,它有一个function伴侣长颈鹿与动物配对,并返回动物。 这绝对不是你想要的。 对于C ++程序员来说交配可能不是很重要,平等就是。 我不知道哪种algorithm不使用某种types的algorithm。

你如何做与ForwardIterator *的比较? 也就是说,你如何检查你所拥有的物品是你想要的,还是已经通过了?

大多数时候,我会用这样的东西:

 void MyFunc(ForwardIterator<MyType>& i) 

这意味着我知道我指向MyType的,我知道如何比较这些。 虽然它看起来像一个模板,但它不是真的(没有“模板”关键字)。

这个问题有很多很好的答案。 还应该提到模板支持开放式devise。 随着面向对象编程语言的现状,在处理这些问题时必须使用访问者模式,而真正的OOP应该支持多种dynamic绑定。 请参阅Open Multi-Methods for C ++,P. Pirkelbauer,et.al. 非常有趣的阅读。

模板的另一个有趣的地方是,它们也可以用于运行时多态性。 例如

 template<class Value,class T> Value euler_fwd(size_t N,double t_0,double t_end,Value y_0,const T& func) { auto dt=(t_end-t_0)/N; for(size_t k=0;k<N;++k) {y_0+=func(t_0 + k*dt,y_0)*dt;} return y_0; } 

注意,如果Value是某种types的向量( 不是 std :: vector,为了避免混淆,应该调用std::dynamic_array

如果func小,这个函数会从内联获得很多。 用法示例

 auto result=euler_fwd(10000,0.0,1.0,1.0,[](double x,double y) {return y;}); 

在这种情况下,你应该知道确切的答案(2.718 …),但是很容易构造一个没有基本解决scheme的简单ODE(提示:在y中使用一个多项式)。

现在,你在func有一个很大的expression式,并且你在很多地方使用了ODE求解器,所以你的可执行文件被模板实例化所污染。 该怎么办? 首先要注意的是常规函数指针的工作原理。 然后你想添加咖喱,所以你写一个接口和一个明确的实例化

 class OdeFunction { public: virtual double operator()(double t,double y) const=0; }; template double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction& func); 

但是上面的实例只能用于double ,为什么不把接口写成模板:

 template<class Value=double> class OdeFunction { public: virtual Value operator()(double t,const Value& y) const=0; }; 

并专门针对一些常见的值types:

 template double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction<double>& func); template vec4_t<double> euler_fwd(size_t N,double t_0,double t_end,vec4_t<double> y_0,const OdeFunction< vec4_t<double> >& func); // (Native AVX vector with four components) template vec8_t<float> euler_fwd(size_t N,double t_0,double t_end,vec8_t<float> y_0,const OdeFunction< vec8_t<float> >& func); // (Native AVX vector with 8 components) template Vector<double> euler_fwd(size_t N,double t_0,double t_end,Vector<double> y_0,const OdeFunction< Vector<double> >& func); // (A N-dimensional real vector, *not* `std::vector`, see above) 

如果这个函数是先devise一个接口的话,那么你将不得不inheritance那个ABC。 现在你有这个选项,以及函数指针,lambda或者其他函数对象。 这里的关键是我们必须有operator()() ,并且我们必须能够在返回types上使用一些算术运算符。 因此,如果C ++没有运算符重载,模板机器将在这种情况下中断。

将接口与接口分离并且能够交换实现的概念不是面向对象编程的固有的概念。 我相信这是一个基于组件开发(如Microsoft COM)的想法。 (关于什么是组件驱动开发,请参阅我的回答 )成长和学习C ++,人们被大肆渲染出inheritance和多态。 直到90年代,人们才开始说“程序到界面”,而不是“实施”,“赞成”对象构成“而不是”阶级inheritance“。 (这两个引用从GoF的方式)。

然后Java随着内置的垃圾回收器和interface关键字一起出现,突然之间实际上将接口和实现分开了。 在你知道之前,这个想法成为了面向对象的一部分。 C ++,模板和STL早于这一切。