为什么 STL 如此严重地基于模板而不是继承?

发布于 2024-07-25 04:37:24 字数 968 浏览 4 评论 0原文

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

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

然后模板和 STL 就开始发挥作用。 STL 似乎采用了经典的 OOP 概念,并使用模板来将它们冲入下水道。

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

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

例如,您可以告诉函数:

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

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

仅通过查看其定义来期望前向迭代器,您需要查看实现或文档:

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

我可以提出支持使用模板的两个主张: 1. 通过重新编译,可以使编译后的代码更加高效每个使用类型的模板,而不是使用动态调度(主要通过 vtable)。 2. 模板可以与本机类型一起使用这一事实。

然而,我正在寻找放弃经典 OOP 而支持 STL 模板的更深刻的原因?

I mean, aside from its name the Standard Template Library (which evolved into the C++ standard library).

C++ initially introduce OOP concepts into C. That is: you could tell what a specific entity could and couldn't do (regardless of how it does it) based on its class and class hierarchy. Some compositions of abilities are more difficult to describe in this manner due to the complexities of multiple inheritance, and the fact that C++ supports interface-only inheritance in a somewhat clumsy way (compared to java, etc), but it's there (and could be improved).

And then templates came into play, along with the STL. The STL seems to take the classical OOP concepts and flush them down the drain, using templates instead.

There should be a distinction between cases when templates are used to generalize types where the types themselves are irrelevant for the operation of the template (containers, for examples). Having a vector<int> makes perfect sense.

However, in many other cases (iterators and algorithms), 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.

For example, you can tell the function:

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

Update: As it was unclear in the original question, ForwardIterator is ok to be templated itself to allow any ForwardIterator type. The contrary is having ForwardIterator as a concept.

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

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

Two claims I can make in favor of using templates: 1. Compiled code can be made more efficient, by recompiling the template for each used type, instead of using dynamic dispatch (mostly via vtables). 2. And the fact that templates can be used with native types.

However, I am looking for a more profound reason for abandoning classic OOP in favor of templating for the STL?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(13

命比纸薄 2024-08-01 04:37:24

简短的回答是“因为 C++ 已经向前发展”。 是的,早在 70 年代末,Stroustrup 就打算创建一个具有 OOP 功能的升级版 C,但那是很久以前的事了。 当该语言于 1998 年标准化时,它不再是 OOP 语言。 这是一种多范式语言。 它当然对 OOP 代码有一定的支持,但它也覆盖了图灵完备的模板语言,它允许编译时元编程,并且人们发现了泛型编程。 突然之间,OOP 似乎不再那么重要了。 当我们可以通过使用模板和泛型编程可用的技术来编写更简单、更简洁和更高效的代码时,情况就不会这样了。

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

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

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

STL 可能不遵循 Java 定义的 OOP 规则,但它是否实现了 OOP 的目标? 不就做到了可重用、低耦合、模块化、封装吗?

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

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

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

然后人们发现,他们添加的模板机制比想象中还要强大。 有人开始尝试使用模板来编写更通用的库。 一种受到函数式编程的启发,一种使用 C++ 的所有新功能。

他将其提交给 C++ 语言委员会,该委员会花了很长时间才习惯它,因为它看起来如此奇怪和不同,但最终意识到它比他们必须包含的传统 OOP 等效项工作得更好< /em>. 因此他们对其进行了一些调整,并将其纳入标准库。

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

无论如何,您提到的支持 STL 的两个原因都是绝对必要的。

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

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

你的问题有一个强有力的假设:OOP 是“最好的”。 我很好奇为什么。 你问他们为什么“放弃经典的 OOP”。 我想知道为什么他们应该坚持下去。 它会有哪些优点?

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.

OOP is not the holy grail. It's a cute idea, and it was quite an improvement over procedural languages back in the 70's when it was invented. But it's honestly not all it's cracked up to be. In many cases it is clumsy and verbose and it doesn't really promote reusable code or modularity.

That is why the C++ community is today far more interested in generic programming, and why everyone is finally starting to realize that functional programming is quite clever as well. OOP on its own just isn't a pretty sight.

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?

The STL may not follow the rules of OOP as Java defines it, but doesn't it achieve the goals of OOP? Doesn't it achieve reusability, low coupling, modularity and encapsulation?

And doesn't it achieve these goals better than an OOP-ified version would?

As for why the STL was adopted into the language, several things happened that led to the STL.

First, templates were added to C++. They were added for much the same reason that generics were added to .NET. It seemed a good idea to be able to write stuff like "containers of a type T" without throwing away type safety. Of course, the implementation they settled on was quite a lot more complex and powerful.

Then people discovered that the template mechanism they had added was even more powerful than expected. And someone started experimenting with using templates to write a more generic library. One inspired by functional programming, and one which used all the new capabilities of C++.

He presented it to the C++ language committee, who took quite a while to grow used to it because it looked so strange and different, but ultimately realized that it worked better than the traditional OOP equivalents they'd have to include otherwise. So they made a few adjustments to it, and adopted it into the standard library.

It wasn't an ideological choice, it wasn't a political choice of "do we want to be OOP or not", but a very pragmatic one. They evaluated the library, and saw that it worked very well.

In any case, both of the reasons you mention for favoring the STL are absolutely essential.

The C++ standard library has to be efficient. If it is less efficient than, say, the equivalent hand-rolled C code, then people would not use it. That would lower productivity, increase the likelihood of bugs, and overall just be a bad idea.

And the STL has to work with primitive types, because primitive types are all you have in C, and they're a major part of both languages. If the STL did not work with native arrays, it would be useless.

Your question has a strong assumption that OOP is "best". I'm curious to hear why. You ask why they "abandoned classical OOP". I'm wondering why they should have stuck with it. Which advantages would it have had?

如日中天 2024-08-01 04:37:24

对于我认为您所问/抱怨的问题,最直接的答案是:C++ 是 OOP 语言的假设是一个错误的假设。

C++ 是一种多范式语言。 它可以使用 OOP 原理进行编程,可以进行过程编程,可以进行通用编程(模板),并且使用 C++11(以前称为 C++0x),某些东西甚至可以进行功能编程。

C++ 的设计者认为这是一个优势,因此他们认为,当泛型编程更好地、更通用地解决问题时,限制 C++ 表现得像纯粹的 OOP 语言,将是一种倒退。 。

The most direct answer to what I think you're asking/complaining about is this: The assumption that C++ is an OOP language is a false assumption.

C++ is a multi-paradigm language. It can be programmed using OOP principles, it can be programmed procedurally, it can be programmed generically (templates), and with C++11 (formerly known as C++0x) some things can even be programmed functionally.

The designers of C++ see this as an advantage, so they would argue that constraining C++ to act like a purely OOP language when generic programming solves the problem better and, well, more generically, would be a step backwards.

情丝乱 2024-08-01 04:37:24

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

这是基本点:算法是在代数结构上定义的。 我又花了几年时间才意识到,必须通过向常规公理添加复杂性要求来扩展结构的概念。 ...我相信迭代器理论对于计算机科学来说是核心,就像环或巴拿赫空间理论对于数学来说是核心一样。 每次我查看一个算法时,我都会尝试找到定义它的结构。 所以我想做的是通用地描述算法。 这就是我喜欢做的事情。 我可以花一个月的时间研究一个众所周知的算法,试图找到它的通用表示。 ...

STL,至少对我来说,代表了唯一可能的编程方式。 事实上,它与 C++ 编程有很大不同,因为它在大多数教科书中都曾出现过并且仍然存在。 但是,你看,我并不是想用 C++ 编程,而是想找到处理软件的正确方法。 ...

我有很多错误的开始。 例如,在我理解为什么该机制存在根本缺陷并且不应该使用之前,我花了数年时间试图找到继承和虚拟的一些用途。 我很高兴没有人能看到所有中间步骤 - 其中大多数都非常愚蠢。

(他确实解释了为什么继承和虚拟——又名面向对象设计“存在根本缺陷,不应该在采访的其余部分使用”)。

一旦 Stepanov 向 Stroustrup 展示了他的库,Stroustrup 和其他人就付出了巨大的努力将其纳入 ISO C++ 标准(同一采访):

Bjarne Stroustrup 的支持至关重要。 Bjarne 确实希望将 STL 纳入标准,如果 Bjarne 想要什么,他就会得到。 ...他甚至强迫我对STL做出一些我永远不会为其他人做的改变...他是我认识的最专一的人。 他把事情做好。 他花了一段时间才理解 STL 的全部内容,但当他理解后,他就准备好将其付诸实践。 他还对 STL 做出了贡献,他坚持认为不止一种编程方式是有效的——十多年来,他反对无休止的批评和炒作,并追求灵活性、效率、重载和类型安全的结合。使 STL 成为可能的模板。 我想非常明确地指出,Bjarne 是我们这一代杰出的语言设计师。

My understanding is that Stroustrup originally preferred an "OOP-styled" container design, and in fact didn't see any other way to do it. Alexander Stepanov is the one responsible for the STL, and his goals did not include "make it object oriented":

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.

(He does explain why inheritance and virtuals -- a.k.a. object oriented design "was fundamentally flawed and should not be used" in the rest of the interview).

Once Stepanov presented his library to Stroustrup, Stroustrup and others went through herculean efforts to get it into the ISO C++ standard (same interview):

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.

热风软妹 2024-08-01 04:37:24

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

是的。 STL 不是面向对象的。 我
认为面向对象是
几乎和人工一样都是骗局
智力。 我还没有看到
有趣的代码出现
来自这些面向对象的人。

The answer is found in this interview with Stepanov, the author of the STL:

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.

送君千里 2024-08-01 04:37:24

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

恕我直言,STL是我见过的最优雅的库:)

对于你的问题,

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

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

我发现 STL 和模板的唯一问题是可怕的错误消息。
这将使用 C++0X 中的概念来解决。

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

Why a pure OOP design to a Data Structure & Algorithms Library would be better ?!
OOP is not the solution for every thing.

IMHO, STL is the most elegant library I have seen ever :)

for your question,

you don't need runtime polymorphism, it is an advantage for STL actually to implement the Library using static polymorphism, that means efficiency.
Try to write a generic Sort or Distance or what ever algorithm that applies to ALL containers!
your Sort in Java would call functions that are dynamic through n-levels to be executed!

You need stupid thing like Boxing and Unboxing to hide nasty assumptions of the so called Pure OOP languages.

The only problem I see with STL, and templates in general is the awful error messages.
Which will be solved using Concepts in C++0X.

Comparing STL to Collections in Java is Like comparing Taj Mahal to my house :)

吲‖鸣 2024-08-01 04:37:24

模板类型应该遵循
一个“概念”(输入迭代器、前向
迭代器等...)实际的位置
定义了概念的细节
完全通过实施
模板函数/类,而不是
与使用的类型的类
模板,这有点
反对 OOP 的使用。

我认为您误解了模板概念的预期用途。 例如,前向迭代器是一个定义非常明确的概念。 要找到使类成为前向迭代器必须有效的表达式及其语义(包括计算复杂性),您可以查看标准或http://www.sgi.com/tech/stl/ForwardIterator.html(您必须点击输入、输出和简单迭代器的链接才能看到这一切)。

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

STL 和 Java 之间处理接口的方式存在三重差异:

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

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

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

第三部分是如果您喜欢一种(编译时)“鸭子类型”:接口可以是隐式的。 在 Java 中,接口在某种程度上是明确的:当且仅当一个类它实现了 Iterable 时,它​​才“是”Iterable。 编译器可以检查其方法的签名是否全部存在且正确,但语义仍然是隐式的(即它们要么被记录,要么没有记录,但只有更多代码(单元测试)才能告诉您实现是否正确)。

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

STL 可能会也可能不会足够符合您的口味,但它确实将接口与实现完全分开。 它确实缺乏 Java 对接口进行反射的能力,并且它以不同的方式报告违反接口要求的情况。

你可以告诉函数...只需要一个前向迭代器
查看它的定义,您需要查看
实施或文档...

我个人认为,如果使用得当,隐式类型是一种优势。 该算法说明了它对其模板参数的作用,并且实现者确保这些事情起作用:这正是“接口”应该做什么的共同点。 此外,对于 STL,您不太可能根据在头文件中查找其前向声明来使用 std::copy 等内容。 程序员应该根据函数的文档来计算函数的含义,而不仅仅是函数签名。 在 C++、Python 或 Java 中都是如此。 使用任何语言键入可以实现的功能都是有限的,并且尝试使用键入来做一些它不做的事情(检查语义)将是一个错误。

也就是说,STL 算法通常以明确需要什么概念的方式命名其模板参数。 然而,这是为了在文档的第一行提供有用的额外信息,而不是为了使前向声明提供更多信息。 您需要了解的内容比参数类型所能封装的要多,因此您必须阅读文档。 (例如,在采用输入范围和输出迭代器的算法中,根据输入范围的大小以及其中的值,输出迭代器可能需要足够的“空间”来容纳一定数量的输出。尝试强类型化。 )

这是 Bjarne 关于显式声明接口的介绍:http://www.artima.com/cppsource/cpp0xP。 html

在泛型中,参数必须是
从接口派生的类(
C++ 相当于接口的是
抽象类)中指定
泛型的定义。 这意味着
所有泛型参数类型都必须
适合一个层次结构。 这强加了
对设计不必要的限制
需要不合理的远见
开发商的一部分。 例如,如果
你写一个泛型,我定义一个
类,人们不能使用我的类作为
除非我知道,否则你的通用论证
关于您指定的接口以及
我的班级就是从中衍生出来的。 那是
严格。

从另一个角度来看,使用鸭子类型,您可以在不知道接口存在的情况下实现该接口。 或者有人可以故意编写一个接口,以便您的类实现它,并查阅您的文档以确保他们不会要求您尚未执行的任何操作。 那很灵活。

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.

I think you misunderstand the intended use of concepts by templates. Forward Iterator, for example, is a very well-defined concept. To find the expressions which must be valid in order for a class to be a Forward Iterator, and their semantics including computational complexity, you look at the standard or at http://www.sgi.com/tech/stl/ForwardIterator.html (you have to follow the links to Input, Output, and Trivial Iterator to see it all).

That document is a perfectly good interface, and "the actual details of the concept" are defined right there. They are not defined by the implementations of Forward Iterators, and neither are they defined by the algorithms which use Forward Iterators.

The differences in how interfaces are handled between STL and Java are three-fold:

1) STL defines valid expressions using the object, whereas Java defines methods which must be callable on the object. Of course a valid expression might be a method (member function) call, but it doesn't have to be.

2) Java interfaces are runtime objects, whereas STL concepts are not visible at runtime even with RTTI.

3) If you fail to make valid the required valid expressions for an STL concept, you get an unspecified compilation error when you instantiate some template with the type. If you fail to implement a required method of a Java interface, you get a specific compilation error saying so.

This third part is if you like a kind of (compile-time) "duck typing": interfaces can be implicit. In Java, interfaces are somewhat explicit: a class "is" Iterable if and only if it says it implements Iterable. The compiler can check that the signatures of its methods are all present and correct, but the semantics are still implicit (i.e. they're either documented or not, but only more code (unit tests) can tell you whether the implementation is correct).

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 may or may not be sufficiently OO for your taste, but it certainly separates interface cleanly from implementation. It does lack Java's ability to do reflection over interfaces, and it reports breaches of interface requirements differently.

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 ...

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.)

Here's Bjarne on explicitly-declared interfaces: 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.

Looking at it the other way around, with duck typing you can implement an interface without knowing that the interface exists. Or someone can write an interface deliberately such that your class implements it, having consulted your docs to see that they don't ask for anything you don't already do. That's flexible.

゛清羽墨安 2024-08-01 04:37:24

“OOP 对我来说仅意味着消息传递、本地保留和保护以及状态过程的隐藏,以及所有事物的极端后期绑定。它可以在 Smalltalk 和 LISP 中完成。可能还有其他系统可以做到这一点,但是我不认识他们。” - Alan Kay,Smalltalk 的创建者。

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

"OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them." - Alan Kay, creator of Smalltalk.

C++, Java, and most other languages are all pretty far from classical OOP. That said, arguing for ideologies is not terribly productive. C++ is not pure in any sense, so it implements functionality that seems to make pragmatic sense at the time.

惜醉颜 2024-08-01 04:37:24

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

只是提供另一个参考:

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

Stepanov 解释了他的工作经验和对大型算法库的选择,最终演变成 STL。

请告诉我们您对泛型编程的长期兴趣

.....然后我得到了贝尔实验室的一份工作,在 C++ 库的 C++ 小组工作。 他们问我是否可以用 C++ 来做。 当然,我不懂 C++,当然,我说我会。 但我无法在 C++ 中做到这一点,因为在 1987 年,C++ 还没有模板,而模板对于实现这种编程风格至关重要。 继承是获得通用性的唯一机制,但还不够。

即使现在,C++ 继承对于泛型编程也没有多大用处。 我们来讨论一下原因。 许多人尝试过使用继承来实现数据结构和容器类。 据我们现在所知,成功的尝试很少。 C++ 继承以及与之相关的编程风格受到极大限制。 使用它来实现包含像平等这样琐碎的事情的设计是不可能的。 如果您从层次结构根部的基类 X 开始,并在该类上定义一个虚拟相等运算符,该运算符采用 X 类型的参数,然后从类 X 派生类 Y。相等的接口是什么? Y与X比较具有相等性。以动物为例(OO人喜爱动物),定义哺乳动物,并从哺乳动物派生出长颈鹿。 然后定义一个成员函数mate,其中animal与animal进行交配并返回一个animal。 然后你从动物中派生出长颈鹿,当然,它有一个 mate 函数,长颈鹿与动物交配并返回动物。 这绝对不是你想要的。 虽然配对对于 C++ 程序员来说可能不是很重要,但平等却很重要。 我不知道有哪个算法不使用某种相等性。

STL started off with the intention of provide a large library covering most commonly used algorithm -- with the target of consitent behavior and performance. Template came as a key factor to make that implementation and target feasible.

Just to provide another reference:

Al Stevens Interviews Alex Stepanov, in March 1995 of DDJ:

Stepanov explained his work experience and choice made towards a large library of algorithm, which eventually evolved into 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.

挖鼻大婶 2024-08-01 04:37:24

基本问题

void MyFunc(ForwardIterator *I);

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

The basic problem with

void MyFunc(ForwardIterator *I);

is how do you safely get the type of the thing the iterator returns? With templates, this is done for you at compile time.

权谋诡计 2024-08-01 04:37:24

让我们暂时将标准库视为基本上是集合和算法的数据库。

如果您研究过数据库的历史,那么您无疑知道,在一开始,数据库大多是“分层的”。 分层数据库与经典的 OOP 非常接近——具体来说,是单继承变体,例如 Smalltalk 使用的。

随着时间的推移,人们逐渐发现分层数据库几乎可以用来建模任何东西,但是在某些情况下单继承模型的局限性相当大。 如果你有一扇木门,那么你可以很方便地将它视为一扇门,或者视为某种原材料(钢、木材等)的一部分。

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

分层模型和网络模型数据库大多已从通用用途中消失(尽管少数仍处于相当特定的领域)。 对于大多数用途,它们已被关系数据库取代。

关系数据库占据主导地位的大部分原因是多功能性。 关系模型在功能上是网络模型的超集(而网络模型又是层次模型的超集)。

C++ 很大程度上遵循了同样的道路。 单继承与层次模型、多重继承与网络模型之间的对应关系是相当明显的。 C++ 模板和分层模型之间的对应关系可能不太明显,但无论如何它都是非常接近的。

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

无论如何,我认为这就是 C++ 不为其容器使用继承的大部分真正原因——没有真正的理由这样做,因为继承仅提供模板提供的功能的子集。 由于模板在某些情况下基本上是必需的,因此它们也可以几乎在任何地方使用。

For a moment, let's think of the standard library as basically a database of collections and algorithms.

If you've studied the history of databases, you undoubtedly know that back in the beginning, databases were mostly "hierarchical". Hierarchical databases corresponded very closely to classical OOP--specifically, the single-inheritance variety, such as used by Smalltalk.

Over time, it became apparent that hierarchical databases could be used to model almost anything, but in some cases the single-inheritance model was fairly limiting. If you had a wooden door, it was handy to be able to look at it either as a door, or as a piece of some raw material (steel, wood, etc.)

So, they invented network model databases. Network model databases correspond very closely to multiple inheritance. C++ supports multiple inheritance completely, while Java supports a limited form (you can inherit from only one class, but can also implement as many interfaces as you like).

Both hierarchical model and network model databases have mostly faded from general purpose use (though a few remain in fairly specific niches). For most purposes, they've been replaced by relational databases.

Much of the reason relational databases took over was versatility. The relational model is functionally a superset of the network model (which is, in turn, a superset of the hierarchical model).

C++ has largely followed the same path. The correspondence between single inheritance and the hierarchical model and between multiple inheritance and the network model are fairly obvious. The correspondence between C++ templates and the hierarchical model may be less obvious, but it's a pretty close fit anyway.

I haven't seen a formal proof of it, but I believe the capabilities of templates are a superset of those provided by multiple inheritance (which is clearly a superset of single inerhitance). The one tricky part is that templates are mostly statically bound--that is, all the binding happens at compile time, not run time. As such, a formal proof that inheritance provides a superset of the capabilities of inheritance may well be somewhat difficult and complex (or may even be impossible).

In any case, I think that's most of the real reason C++ doesn't use inheritance for its containers--there's no real reason to do so, because inheritance provides only a subset of the capabilities provided by templates. Since templates are basically a necessity in some cases, they might as well be used nearly everywhere.

剩一世无双 2024-08-01 04:37:24

这个问题有很多很好的答案。 还应该提到的是,模板支持开放式设计。 以面向对象编程语言的当前状态,在处理此类问题时必须使用访问者模式,并且真正的OOP应该支持多个动态绑定。 请参阅 Open Multi-Methods for 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< /code> 以避免混淆)

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

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

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

现在,您在 func 中有一个很大的表达式,并且在许多地方使用 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;
    };

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

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++ 没有运算符重载,模板机制在这种情况下就会崩溃。

This question has many great answers. It should also be mentioned that templates supports an open design. With the current state of object oriented programming languages, one has to use the visitor pattern when dealing with such problems, and true OOP should support multiple dynamic binding. See Open Multi-Methods for C++, P. Pirkelbauer, et.al. for very intersting reading.

Another interesting point of templates are that they can be used on for runtime polymorphism as well. For example

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

Notice that this function will also work if Value is a vector of some kind (not std::vector, which should be called std::dynamic_array to avoid confusion)

If func is small, this function will gain a lot from inlining. Example usage

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

In this case, you should know the exact answer (2.718...), but it is easy to construct a simple ODE without elementary solution (Hint: use a polynomial in y).

Now, you have a large expression in func, and you use the ODE solver in many places, so your executable gets polluted with template instantiations everywhere. What to do? First thing to notice is that a regular function pointer works. Then you want to add currying so you write an interface and an explicit instantiation

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

But the above instantiation only works for double, why not write the interface as template:

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

and specialize for some common value 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)

If the function had been designed around an interface first, then you would have been forced to inherit from that ABC. Now you have this option, as well as function pointer, lambda, or any other function object. The key here is that we must have operator()(), and we must be able to do use some arithmetic operators on its return type. Thus, the template machinery would break in this case if C++ did not have operator overloading.

八巷 2024-08-01 04:37:24

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

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

void MyFunc(ForwardIterator<MyType>& i)

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

How do you do comparisons with ForwardIterator*'s? That is, how do you check if the item you have is what you're looking for, or you've passed it by?

Most of the time, I would use something like this:

void MyFunc(ForwardIterator<MyType>& i)

which means I know that i is pointing to MyType's, and I know how to compare those. Though it looks like a template, it isn't really (no "template" keyword).

初心未许 2024-08-01 04:37:24

将接口与接口分离以及能够交换实现的概念并不是面向对象编程所固有的。 我相信这是在基于组件的开发(如 Microsoft COM)中孕育出来的想法。 (请参阅我对什么是组件驱动开发?的回答)在学习 C++ 的过程中,人们对继承和多态性大肆宣传。 直到 90 年代,人们才开始说“针对‘接口’编程,而不是‘实现’”和“优先考虑‘对象组合’而不是‘类继承’”。 (顺便说一句,这两者都引用自GoF)。

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

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).

Then Java came along with built-in garbage collector and interface keyword, and all of a sudden it became practical to actually separate interface and implementation. Before you know it the idea became part of the OO. C++, templates, and STL predates all of this.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文