我的意思是,除了它的名字标准模板库(后来演变成c++标准库)。

c++最初将面向对象的概念引入到C中,也就是说:您可以根据特定实体的类和类层次结构来判断它能做什么和不能做什么(不管它是如何做的)。由于多重继承的复杂性,一些能力的组合更难以用这种方式描述,而且c++以一种有点笨拙的方式支持仅接口继承(与java等相比),但它确实存在(并且可以改进)。

然后模板开始发挥作用,还有STL。STL似乎采用了经典的OOP概念,并将它们冲入下水道,取而代之的是使用模板。

当模板用于泛化类型时,类型本身与模板的操作无关(例如容器),这两种情况之间应该有区别。vector<int>非常有意义。

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

例如,你可以告诉函数:

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

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

只有通过查看它的定义才能期望Forward Iterator,在这里你需要查看以下方面的实现或文档:

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

我可以提出两个主张来支持使用模板:通过为每个使用的类型重新编译模板,而不是使用动态分派(主要通过虚表),可以使编译后的代码更加高效。2. 事实上,模板可以与原生类型一起使用。

然而,我正在寻找一个更深刻的原因,放弃经典的面向对象的支持模板的STL?


对于我认为你在问或抱怨的问题,最直接的回答是:假设c++是一种面向对象语言是一个错误的假设。

c++是一种多范式语言。它可以使用面向对象原则编程,可以程序化编程,也可以泛型编程(模板),使用c++ 11(以前称为c++ 0x)甚至可以对某些东西进行函数式编程。

c++的设计者认为这是一种优势,所以他们会争辩说,当泛型编程能更好地解决问题,并且更通用地解决问题时,限制c++像纯粹的面向对象语言一样运行将是一种倒退。


最基本的问题是

void MyFunc(ForwardIterator *I);

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


The short answer is "because C++ has moved on". Yes, back in the late 70's, Stroustrup intended to create an upgraded C with OOP capabilities, but that is a long time ago. By the time the language was standardized in 1998, it was no longer an OOP language. It was a multi-paradigm language. It certainly had some support for OOP code, but it also had a turing-complete template language overlaid, it allowed compile-time metaprogramming, and people had discovered generic programming. Suddenly, OOP just didn't seem all that important. Not when we can write simpler, more concise and more efficient code by using techniques available through templates and generic programming.

面向对象编程不是圣杯。这是一个可爱的想法,它比70年代的过程语言有了很大的改进。但说实话,它并不像人们说的那么好。在许多情况下,它是笨拙和冗长的,并不能真正促进可重用代码或模块化。

这就是为什么c++社区今天对泛型编程更感兴趣,也是为什么每个人终于开始意识到函数式编程也是相当聪明的。OOP本身并不是一个美丽的景象。

Try drawing a dependency graph of a hypothetical "OOP-ified" STL. How many classes would have to know about each other? There would be a lot of dependencies. Would you be able to include just the vector header, without also getting iterator or even iostream pulled in? The STL makes this easy. A vector knows about the iterator type it defines, and that's all. The STL algorithms know nothing. They don't even need to include an iterator header, even though they all accept iterators as parameters. Which is more modular then?

STL可能不像Java定义的那样遵循面向对象的规则,但它不也达到了面向对象的目标吗?难道它没有实现重用性、低耦合性、模块化和封装性吗?

它能比oop化的版本更好地实现这些目标吗?

至于为什么STL被采用到语言中,发生了几件事导致了STL的出现。

首先,将模板添加到c++中。添加它们的原因与。net中添加泛型的原因大致相同。能够在不抛弃类型安全性的情况下编写“T类型容器”之类的东西似乎是个好主意。当然,他们确定的实现要复杂得多,功能也强大得多。

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

他把它提交给c++语言委员会,委员会花了很长时间才适应它,因为它看起来很奇怪,很不一样,但最终意识到它比传统的面向对象编程更好。所以他们对它做了一些调整,并将其纳入标准库。

这不是一个意识形态的选择,也不是一个“我们想不想成为面向对象”的政治选择,而是一个非常务实的选择。他们评估了这个图书馆,发现它运行得很好。

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

c++标准库必须是高效的。如果它的效率低于,比如说,同等的手工编写的C代码,那么人们就不会使用它。这将降低生产力,增加bug的可能性,并且总体来说是个坏主意。

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

你的问题强烈地假设面向对象编程是“最好的”。我很好奇为什么。你问为什么他们“放弃了经典的面向对象编程”。我在想他们为什么要坚持下去。它会有哪些优势?


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

大多数时候,我会使用这样的方法:

void MyFunc(ForwardIterator<MyType>& i)

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


为什么数据结构和算法库的纯面向对象设计会更好?! 面向对象编程并不是所有事情的解决方案。

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

关于你的问题,

你不需要运行时多态性,STL实际上使用静态多态性实现库是一个优势,这意味着效率。 试着写一个通用的Sort或Distance或任何适用于所有容器的算法! 你在Java中的Sort会调用动态的n级函数来执行!

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

我在STL和模板中看到的唯一问题是糟糕的错误消息。 这将使用c++ 0X中的概念来解决。

将STL与Java中的集合进行比较,就像将泰姬陵与我的房子进行比较:)


“面向对象编程对我来说只意味着消息传递、状态过程的本地保留、保护和隐藏,以及所有事情的极端后期绑定。它可以在Smalltalk和LISP中完成。可能还有其他系统也有这种可能,但我不知道它们。”——Alan Kay (Smalltalk的创造者)

c++、Java和大多数其他语言都与经典的OOP相去甚远。也就是说,为意识形态争论并不是特别有效。c++在任何意义上都不是纯粹的,所以它实现的功能在当时似乎是有实用意义的。


我的理解是,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.

(在接下来的采访中,他解释了为什么继承和虚拟——也就是面向对象的设计“从根本上存在缺陷,不应该被使用”)。

当Stepanov将他的库展示给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的采访中可以找到答案:

是的。STL不是面向对象的。我 认为面向对象是 几乎和人造的一样是骗局 情报。我还没见过 这段有趣的代码 来自这些面向对象的人。


模板类型应该紧随其后 一个“概念”(输入迭代器,前进 迭代器,等等… 定义了概念的细节 完全由实现的 模板函数/类,而不是由 类所使用的类型的类 模板,这是一个 反对使用面向对象编程。

我认为您误解了模板对概念的预期用途。例如,前向迭代器是一个定义非常明确的概念。要找到使类成为前向迭代器必须有效的表达式,以及它们的语义(包括计算复杂性),可以查看标准或http://www.sgi.com/tech/stl/ForwardIterator.html(必须按照Input、Output和Trivial Iterator的链接查看所有内容)。

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

STL和Java处理接口的方式有三个不同之处:

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

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

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

第三部分是如果你喜欢某种(编译时)"duck typing":接口可以是隐式的。在Java中,接口是显式的:当且仅当一个类说它实现了Iterable时,它才是Iterable。编译器可以检查它的方法的签名是否都存在并且正确,但是语义仍然是隐式的(即它们要么被记录下来,要么没有,但只有更多的代码(单元测试)才能告诉你实现是否正确)。

In C++, like in Python, both semantics and syntax are implicit, although in C++ (and in Python if you get the strong-typing preprocessor) you do get some help from the compiler. If a programmer requires Java-like explicit declaration of interfaces by the implementing class, then the standard approach is to use type traits (and multiple inheritance can prevent this being too verbose). What's lacking, compared with Java, is a single template which I can instantiate with my type, and which will compile if and only if all the required expressions are valid for my type. This would tell me whether I've implemented all the required bits, "before I use it". That's a convenience, but it's not the core of OOP (and it still doesn't test semantics, and code to test semantics would naturally also test the validity of the expressions in question).

STL对你来说可能是OO,也可能不是,但它确实把接口和实现清晰地分开了。它确实缺乏Java在接口上进行反射的能力,并且它以不同的方式报告违反接口需求的情况。

你可以告诉函数。期望前向迭代器仅为 看它的定义,你需要看 实现或文档…

Personally I think that implicit types are a strength, when used appropriately. The algorithm says what it does with its template parameters, and the implementer makes sure those things work: it's exactly the common denominator of what "interfaces" should do. Furthermore with STL, you're unlikely to be using, say, std::copy based on finding its forward declaration in a header file. Programmers should be working out what a function takes based on its documentation, not just on the function signature. This is true in C++, Python, or Java. There are limitations on what can be achieved with typing in any language, and trying to use typing to do something it doesn't do (check semantics) would be an error.

That said, STL algorithms usually name their template parameters in a way which makes it clear what concept is required. However this is to provide useful extra information in the first line of the documentation, not to make forward declarations more informative. There are more things you need to know than can be encapsulated in the types of the parameters, so you have to read the docs. (For example in algorithms which take an input range and an output iterator, chances are the output iterator needs enough "space" for a certain number of outputs based on the size of the input range and maybe the values therein. Try strongly typing that.)

以下是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.

从另一个角度来看,使用duck类型,您可以在不知道接口存在的情况下实现接口。或者有人可以故意编写一个接口,让你的类实现它,并咨询你的文档,看看他们不会要求任何你没有做过的事情。这是灵活的。


The concept of separating interface from interface and being able to swap out the implementations is not intrinsic to Object-Oriented Programming. I believe it's an idea that was hatched in Component-Based Development like Microsoft COM. (See my answer on What is Component-Driven Development?) Growing up and learning C++, people were hyped out inheritance and polymorphism. It wasn't until 90s people started to say "Program to an 'interface', not an 'implementation'" and "Favor 'object composition' over 'class inheritance'." (both of which quoted from GoF by the way).

然后,Java出现了内置的垃圾收集器和接口关键字,突然之间,将接口和实现分开变得可行了。不知不觉,这个想法就成为了OO的一部分。c++、模板和STL的出现要早于这一切。


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

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

1995年3月,《DDJ》的艾尔·史蒂文斯采访亚历克斯·斯捷潘诺夫:

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

Stepanov解释了他的工作经验和对大型算法库的选择,最终演变为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.


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

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

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很小,这个函数将从内联中获得很多。示例使用

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

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

现在,您在func中有一个大表达式,并且在许多地方使用ODE求解器,因此您的可执行文件到处都受到模板实例化的污染。怎么办呢?首先要注意的是,常规函数指针可以工作。然后,您希望添加curry,以便编写接口和显式实例化

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;
    };

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

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++没有操作符重载,模板机制就会中断。


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

如果您研究过数据库的历史,那么您无疑知道,在最初,数据库大多是“分层的”。分层数据库与经典的面向对象编程(OOP)非常相似——特别是单一继承类型,例如Smalltalk所使用的那种。

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

因此,他们发明了网络模型数据库。网络模型数据库非常接近于多重继承。c++完全支持多重继承,而Java只支持有限的形式(只能继承一个类,但也可以实现任意多的接口)。

层次模型和网络模型数据库都已经从通用用途中消失了(尽管有一些仍然在相当特定的利基领域)。在大多数情况下,它们已经被关系数据库所取代。

关系数据库取代它的主要原因是它的多功能性。关系模型在功能上是网络模型的超集(网络模型又是层次模型的超集)。

c++在很大程度上遵循了同样的路径。单继承与层次模型、多继承与网络模型的对应关系较为明显。c++模板和层次模型之间的对应关系可能不太明显,但无论如何都非常接近。

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

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