关于OOP:为什么C ++ STL基于模板如此重要? (而不是*接口*)

Why is the C++ STL is so heavily based on templates? (and not on *interfaces*)

我的意思是,除了它的强制名称(标准模板库)之外…

C++最初打算将OOP概念呈现到C中,即:根据特定的类和类层次结构,可以知道特定的实体可以做什么和不能做什么(不管它是如何做的)。由于多重继承的问题,以及C++以一种有点笨拙的方式支持接口的概念(与Java等相比),某些能力的组成更难以用这种方式来描述,但它确实存在(并且可以改进)。

然后模板和STL一起发挥作用。STL似乎采用了经典的OOP概念,并使用模板将其彻底清除。

当模板用于归纳类型时,如果类型主题本身与模板的操作无关(例如,容器),则应区分这两种情况。拥有一个vector是完全合理的。

但是,在许多其他情况下(迭代器和算法),模板化类型应该遵循"概念"(输入迭代器、前向迭代器等),其中概念的实际细节完全由模板函数/类的实现来定义,而不是由与模板一起使用的类型的类来定义,这是一个有点反U的类型。OOP的圣人。

例如,您可以告诉函数:

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

更新:由于在原始问题中不清楚,可以将ForwardIterator本身模板化以允许任何ForwardIterator类型。相反,将ForwardIterator作为一个概念。

只通过查看其定义来期望一个前向迭代器,在这里您需要查看实现或文档:

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

我可以提出两个有利于使用模板的主张:通过为每种使用的类型定制模板,而不是使用vtables,编译后的代码可以变得更高效。模板可以与本机类型一起使用。

然而,我在寻找一个更深刻的原因,为什么放弃经典的OOP,转而为STL模板化?(假设你读了那么多:p)


简短的回答是"因为C++已经开始了"。是的,早在70年代末,stroustrup就打算创建一个具有OOP功能的升级C,但那是很久以前的事了。到1998年语言标准化时,它已不再是OOP语言。它是一种多范式的语言。它当然对OOP代码有一些支持,但是它也覆盖了一个图灵完整的模板语言,它允许编译时元编程,人们发现了通用编程。突然间,哎呀,似乎并不那么重要。当我们可以使用模板和通用编程提供的技术来编写更简单、更简洁和更高效的代码时,就不是这样了。

OOP不是圣杯。这是一个很可爱的想法,它比70年代发明的程序语言有了很大的改进。但老实说,这并不是人们所说的那样。在许多情况下,它是笨拙和冗长的,它并没有真正促进可重用代码或模块化。

这就是为什么C++社区今天对泛型编程更感兴趣的原因,以及为什么每个人终于开始意识到函数式编程也是相当聪明的。哎呀,一个人不怎么好看。

尝试绘制一个假设的"OOP化"STL的依赖关系图。有多少个班必须互相了解?会有很多依赖关系。您是否可以只包含vector头文件,而不需要引入iterator甚至iostream?STL使这变得容易。向量知道它定义的迭代器类型,仅此而已。STL算法一无所知。它们甚至不需要包含迭代器头,即使它们都接受迭代器作为参数。那么哪个更模块化呢?

当Java定义它时,STL可能不遵循OOP规则,但是它没有实现面向对象编程的目标吗?它是否实现了可重用性、低耦合、模块化和封装?

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

至于为什么在语言中采用STL,有几件事导致了STL。

首先,将模板添加到C++中。它们被添加的原因与泛型被添加到.NET的原因大致相同。在不丢掉类型安全性的情况下,写"t型容器"之类的东西似乎是个好主意。当然,他们确定的实现要复杂得多,功能也要强大得多。

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

他把它提交给C++语言委员会,他们花了相当长的时间才习惯它,因为它看起来如此奇怪和不同,但最终意识到它比传统的OOP等价物要好得多。所以他们对它做了一些调整,并将其应用到标准库中。

这不是一个意识形态上的选择,也不是一个政治上的选择"我们是否想成为OOP",而是一个非常务实的选择。他们评估了图书馆,发现它运作得很好。

在任何情况下,您提到的支持STL的两个原因都是绝对必要的。

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

STL必须使用基元类型,因为基元类型是C语言中的所有类型,它们是两种语言的主要部分。如果STL不能与本机数组一起工作,它将是无用的。

你的问题有一个很强的假设,即OOP是"最好的"。我很好奇为什么。你问他们为什么"放弃了古典的OOP"。我想知道他们为什么要坚持下去。它有哪些优势?


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

C++是一种多范式语言。它可以用OOP原理编程,它可以程序化编程,它可以通用编程(模板),用C++ 11(以前称为C++ 0x),有些东西甚至可以在功能上编程。

C++的设计者认为这是一个优势,所以他们会认为,当泛型编程更好地解决这个问题时,约束C++充当纯OOP语言,而且,更一般地说,将是一个倒退。


我的理解是,stroustrup最初更喜欢"oop风格"的容器设计,事实上,它没有看到任何其他的方法。Alexander Stepanov是负责STL的人,他的目标不包括"使其面向对象"。

That is the fundamental point: algorithms are defined on algebraic structures. It took me another couple of years to realize that you have to extend the notion of structure by adding complexity requirements to regular axioms. ... I believe that iterator theories are as central to Computer Science as theories of rings or Banach spaces are central to Mathematics. Every time I would look at an algorithm I would try to find a structure on which it is defined. So what I wanted to do was to describe algorithms generically. That's what I like to do. I can spend a month working on a well known algorithm trying to find its generic representation. ...

STL, at least for me, represents the only way programming is possible. It is, indeed, quite different from C++ programming as it was presented and still is presented in most textbooks. But, you see, I was not trying to program in C++, I was trying to find the right way to deal with software. ...

I had many false starts. For example, I spent years trying to find some use for inheritance and virtuals, before I understood why that mechanism was fundamentally flawed and should not be used. I am very happy that nobody could see all the intermediate steps - most of them were very silly.

(他确实解释了为什么继承和虚拟——也就是说面向对象的设计"从根本上来说是有缺陷的,在采访的其余部分不应该使用")。

一旦斯特潘诺夫把他的图书馆介绍给Stroustrup,Stroustrup和其他人就通过努力将其纳入ISO C++标准(同样的采访):

The support of Bjarne Stroustrup was crucial. Bjarne really wanted STL in the standard and if Bjarne wants something, he gets it. ... He even forced me to make changes in STL that I would never make for anybody else ... he is the most single minded person I know. He gets things done. It took him a while to understand what STL was all about, but when he did, he was prepared to push it through. He also contributed to STL by standing up for the view that more than one way of programming was valid - against no end of flak and hype for more than a decade, and pursuing a combination of flexibility, efficiency, overloading, and type-safety in templates that made STL possible. I would like to state quite clearly that Bjarne is the preeminent language designer of my generation.


答案可以在对STL作者Stepanov的采访中找到:

Yes. STL is not object oriented. I
think that object orientedness is
almost as much of a hoax as Artificial
Intelligence. I have yet to see an
interesting piece of code that comes
from these OO people.


为什么对数据结构和算法库进行纯OOP设计会更好?!OOP不是解决所有问题的方法。

imho,STL是我见过的最优雅的图书馆:)

关于你的问题,

您不需要运行时多态性,STL实际上使用静态多态性实现库是一个优势,这意味着效率。尝试写一个通用的排序或距离,或什么样的算法,适用于所有容器!Java中的排序将调用通过N级动态执行的函数!

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

我在STL和模板中看到的唯一问题通常是可怕的错误消息。这将用C++中的概念来解决。

将STL与Java中的集合进行比较就好比把泰姬陵比作我的房子:


templated types are supposed to follow
a"concept" (Input Iterator, Forward
Iterator, etc...) where the actual
details of the concept are defined
entirely by the implementation of the
template function/class, and not by
the class of the type used with the
template, which is a somewhat
anti-usage of OOP.

Ok.

我认为你误解了模板对概念的预期用途。例如,前向迭代器是一个定义很好的概念。为了找到类成为前向迭代器必须有效的表达式,以及它们的语义(包括计算复杂性),您可以查看标准或http://www.sgi.com/tech/stl/forward迭代器.html(您必须按照输入、输出和琐碎迭代器的链接来查看全部内容)。好的。

该文档是一个非常好的界面,"概念的实际细节"就在这里定义。它们不是由前向迭代器的实现定义的,也不是由使用前向迭代器的算法定义的。好的。

STL和Java之间如何处理接口的差异有三个方面:好的。

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

2)Java接口是运行时对象,而STL概念在运行时甚至在RTTI中也看不见。好的。

3)如果未能使STL概念所需的有效表达式有效,则在用类型实例化某个模板时会出现未指定的编译错误。如果无法实现Java接口所需的方法,那么会得到一个特定的编译错误。好的。

第三部分是如果您喜欢一种(编译时)"duck-typing":接口可以是隐式的。在爪哇中,接口有点明确:一个类"是"可迭代的,当且仅当它表示它实现迭代时。编译器可以检查其方法的签名是否都存在并且正确,但是语义仍然是隐式的(即它们是否有文档记录,但是只有更多的代码(单元测试)可以告诉您实现是否正确)。好的。

在C++中,类似于Python,语义和语法都是隐式的,尽管在C++中(在Python中,如果你得到强类型的预处理器),你也会从编译器那里得到一些帮助。如果程序员需要实现类的Java类接口显式声明,那么标准方法是使用类型特征(并且多重继承可以防止这太冗长)。与Java相比,缺少的是一个模板,我可以用我的类型实例化它,它将编译,当且仅当所有所需表达式对我的类型有效时。这将告诉我是否已经实现了所有必需的位,"在我使用它之前"。这是一种方便,但它不是OOP的核心(它仍然不测试语义,代码到测试语义自然也会测试相关表达式的有效性)。好的。

STL可能不适合您的口味,也可能不适合您的口味,但它确实将接口与实现完全分离。它缺乏Java对接口进行反射的能力,并且它不同地报告接口要求的违反。好的。

you can tell the function ... expects a Forward Iterator only by
looking at its definition, where you'd need either to look at the
implementation or the documentation for ...

Ok.

我个人认为,适当地使用隐式类型是一种优势。该算法说明了它对模板参数的作用,实现人员确保这些功能正常工作:它正是"接口"应该做什么的公分母。此外,对于stl,基于在头文件中查找其转发声明,您不太可能使用std::copy。程序员应该根据函数的文档,而不仅仅是函数签名,来计算函数需要什么。这在C++、Python或Java中是正确的。在任何语言中输入都有局限性,尝试使用输入来做它不做的事情(检查语义)是错误的。好的。

也就是说,STL算法通常以一种方式命名它们的模板参数,从而清楚地知道需要什么概念。然而,这是为了在文档的第一行中提供有用的额外信息,而不是为了使前面的声明更具信息性。您需要知道的东西比可以封装在参数类型中的要多,所以您必须阅读文档。(例如,在采用输入范围和输出迭代器的算法中,输出迭代器很可能需要足够的"空间",以便根据输入范围的大小和其中的值为一定数量的输出提供空间)。尝试强烈地输入。)好的。

下面是关于显式声明接口的bjarne:http://www.artima.com/cppsource/cpp0xp.html好的。

In generics, an argument must be of a
class derived from an interface (the
C++ equivalent to interface is
abstract class) specified in the
definition of the generic. That means
that all generic argument types must
fit into a hierarchy. That imposes
unnecessary constraints on designs
requires unreasonable foresight on the
part of developers. For example, if
you write a generic and I define a
class, people can't use my class as an
argument to your generic unless I knew
about the interface you specified and
had derived my class from it. That's
rigid.

Ok.

从另一个角度来看,通过duck输入,您可以在不知道接口存在的情况下实现接口。或者,有人可以故意编写一个接口,这样您的类就可以实现它,在咨询了您的文档之后,他们就不会要求您已经做过的任何事情了。这很灵活。好的。好啊。


"对我来说,OOP只意味着消息传递、本地保留、保护和隐藏状态过程,以及所有事情的极端延迟绑定。它可以在smalltalk和lisp中完成。可能还有其他的系统可以实现这一点,但我不知道它们。"—Alan Kay,Smalltalk的创建者。

C++、Java和其他大多数语言都与经典OOP非常相距甚远。这就是说,主张意识形态并没有多大成效。C++在任何意义上都不是纯的,因此它实现了在当时看来具有实用意义的功能。


STL最初的目的是提供一个涵盖最常用算法的大型库——以一致的行为和性能为目标。模板是使实现和目标可行的关键因素。

只是为了提供另一个参考:

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

  • 网址:http://www.sgi.com/tech/stl/drdobbs-interview.html

斯蒂芬诺夫解释了他的工作经验和对一个大型算法库的选择,最终演变成STL。

Tell us something about your long-term interest in generic programming

.....Then I was offered a job at Bell Laboratories working in the C++ group on C++ libraries. They asked me whether I could do it in C++. Of course, I didn't know C++ and, of course, I said I could. But I couldn't do it in C++, because in 1987 C++ didn't have templates, which are essential for enabling this style of programming. Inheritance was the only mechanism to obtain genericity and it was not sufficient.

Even now C++ inheritance is not of much use for generic programming. Let's discuss why. Many people have attempted to use inheritance to implement data structures and container classes. As we know now, there were few if any successful attempts. C++ inheritance, and the programming style associated with it are dramatically limited. It is impossible to implement a design which includes as trivial a thing as equality using it. If you start with a base class X at the root of your hierarchy and define a virtual equality operator on this class which takes an argument of the type X, then derive class Y from class X. What is the interface of the equality? It has equality which compares Y with X. Using animals as an example (OO people love animals), define mammal and derive giraffe from mammal. Then define a member function mate, where animal mates with animal and returns an animal. Then you derive giraffe from animal and, of course, it has a function mate where giraffe mates with animal and returns an animal. It's definitely not what you want. While mating may not be very important for C++ programmers, equality is. I do not know a single algorithm where equality of some kind is not used.


与…有关的基本问题

1
void MyFunc(ForwardIterator *I);

您如何安全地获取迭代器返回的内容类型?对于模板,这是在编译时为您完成的。


现在,让我们把标准库看作是集合和算法的基本数据库。

如果您研究过数据库的历史,毫无疑问您早在一开始就知道,数据库主要是"分层的"。层次数据库非常接近于经典的OOP——特别是单一继承变体,如smalltalk所使用的。

随着时间的推移,层次数据库可以用来建模几乎任何东西,但在某些情况下,单一继承模型是相当有限的。如果你有一扇木门,你可以把它看成是一扇门,或者是一块原材料(钢、木头等),这很方便。

于是,他们发明了网络模型数据库。网络模型数据库与多重继承关系密切。C++完全支持多重继承,而Java支持一种有限的形式(您只能从一个类继承,但也可以实现尽可能多的接口)。

层次模型和网络模型数据库大多已经从一般用途上消失(尽管有一些仍然保留在相当特定的位置)。在大多数情况下,它们都被关系数据库所取代。

关系数据库接管的大部分原因是多功能性。关系模型在功能上是网络模型的超集(反过来又是层次模型的超集)。

C++基本上遵循相同的路径。单继承与层次模型、多重继承与网络模型的对应关系比较明显。C++模板和层次模型之间的对应关系可能不那么明显,但它还是非常贴近。

我没有看到它的正式证明,但我相信模板的功能是由多重继承提供的那些功能的超集(这显然是单一惰性的超集)。一个棘手的部分是,模板大多是静态绑定的——也就是说,所有绑定都发生在编译时,而不是运行时。因此,继承提供了继承能力超集的正式证据可能有点困难和复杂(甚至可能是不可能的)。

在任何情况下,我认为这是C++不为其容器使用继承的真正原因——因为继承只提供模板所提供的能力的子集,所以没有真正的理由这么做。由于模板在某些情况下基本上是必需的,所以它们也可以在几乎所有地方使用。


这个问题有许多很好的答案。还应该提到,模板支持开放式设计。在面向对象编程语言的当前状态下,在处理这些问题时必须使用访问者模式,而真正的OOP应该支持多个动态绑定。参见C++、P. Pirkelbauer、ET.AL的开放多方法。非常有趣的阅读。

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

1
2
3
4
5
6
7
8
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是某种类型的向量(而不是std::vector,应该称为std::dynamic_array以避免混淆),则此函数也可以工作。

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

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

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

现在,在func中有一个大型表达式,并且在许多地方使用了ode解算器,因此您的可执行文件会受到模板实例化的污染。怎么办?首先要注意的是,常规函数指针可以工作。然后要添加curring,这样就可以编写接口和显式实例化

1
2
3
4
5
6
7
8
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,为什么不把接口写为模板呢?

1
2
3
4
5
6
template<class Value=double>
class OdeFunction
    {
    public:
        virtual Value operator()(double t,const Value& y) const=0;
    };

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

1
2
3
4
5
6
7
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)

如果函数是先围绕一个接口设计的,那么您将被强制从该ABC继承。现在您有了这个选项,以及函数指针、lambda或任何其他函数对象。这里的关键是我们必须有operator()(),并且我们必须能够对其返回类型使用一些算术运算符。因此,如果C++没有操作符重载,那么模板机械将在这种情况下中断。


如何与ForwardIterator*进行比较?也就是说,你如何检查你所拥有的物品是你正在寻找的,还是你已经通过了?

大多数情况下,我会用这样的方法:

1
void MyFunc(ForwardIterator<MyType>& i)

这意味着我知道我指的是我的类型,我知道如何比较它们。虽然它看起来像一个模板,但实际上不是(没有"template"关键字)。


将接口与接口分离并能够交换实现的概念不是面向对象编程的固有概念。我相信这是一个在基于组件的开发中孵化出来的想法,比如微软公司。(请参阅我关于什么是组件驱动开发的答案?)成长和学习C++,人们被炒作继承和多态。直到90年代人们才开始说"程序到接口",而不是"实现","喜欢对象组合而不是类继承"。(这两个都引用了GoF的话)。

然后Java与内置的垃圾收集器和EDCOX1,0,关键字,并突然变得实用的实际分离接口和实现。在你意识到之前,这个想法已经成为OO的一部分。C++、模板和STL都早于此。