C++ 为什么不是';标准库容器是否有一个共同的基础?

C++ 为什么不是';标准库容器是否有一个共同的基础?,c++,stl,c++-standard-library,C++,Stl,C++ Standard Library,只是出于兴趣 如果我要设计一个容器库,我肯定会从一个公共基类派生它们,该基类将具有(可能是抽象的)方法声明,如size()和insert() 标准库容器不这样实现有充分的理由吗?没有充分的理由引入这样的基类。谁会从中受益?它在哪里有用 需要“通用”接口的算法使用迭代器。没有将元素插入不同容器的通用方法。事实上,甚至不能将元素插入到一些容器中。 C++中,继承用于运行时多态性(Read:运行时接口重用)。它附带了运行时通过vtable重定向的开销 为了使多个容器类具有相同的接口(这样API是可预测

只是出于兴趣

如果我要设计一个容器库,我肯定会从一个公共基类派生它们,该基类将具有(可能是抽象的)方法声明,如
size()
insert()


标准库容器不这样实现有充分的理由吗?

没有充分的理由引入这样的基类。谁会从中受益?它在哪里有用


需要“通用”接口的算法使用迭代器。没有将元素插入不同容器的通用方法。事实上,甚至不能将元素插入到一些容器中。

C++中,继承用于运行时多态性(Read:运行时接口重用)。它附带了运行时通过vtable重定向的开销

为了使多个容器类具有相同的接口(这样API是可预测的,算法可以做出假设),不需要继承。只要给他们同样的方法,你就完成了。C++中的容器(和算法)被实现为模板,这意味着你得到编译时多态性。
<>这避免了任何运行时开销,它符合C++咒语“只为你需要的东西付费”。

如果你可以编写与你使用的容器无关的代码,那就有意义了。但事实并非如此。我建议您阅读《有效STL》一书中的本章“第2项:谨防容器独立代码的幻觉”。从另一个角度来看:现在,如果您以通用方式使用它们,编译器将拥有它所能拥有的所有信息,从而实现彻底的优化。多亏了模板实现

现在,例如,如果list和vector都实现了一个抽象的OO接口,比如push_backable,并且在代码中使用了一个抽象指针(push_backable*)->push_backable(…),编译器将丢失大量信息,从而失去优化的机会

这些是可能出现在内部循环中的典型操作,您确实希望对它们进行最大可能的优化。另请参见Frerich Raabe的回答。

Java集合(您可能已经想到)在
AbstractCollection
中实现了一系列通用方法,这些方法位于具体类中实现的
size()
iterator()
方法之上。然后IIRC在
AbstractList
等中有更多这样的方法。有些子类覆盖了这些方法中的大多数,有些子类可以实现所需的抽象方法之外的一些方法。有些方法在共享一个接口的大多数或所有集合中都是共同实现的。例如,在我痛苦的经历中,给你一个不可修改视图的是一个要写的完整PITA

C++容器有几个与所有容器相同的成员函数,几乎没有一个可以以相同的方式对所有容器[*]实现。如果存在可以使用迭代器执行的常见算法(如
查找
),则它们是
中的自由函数,而不是继承可以查看的任何地方

所有序列、所有关联数组等都有共同的成员函数。但对于这些概念,不同具体数据结构的实现之间没有太多共同之处

因此,归根结底,这是一个设计哲学的问题。java和C++都有一些关于容器的代码重用。在C++中,代码重用是通过代码< <代码> /COD>中的函数模板实现的。在Java中,有些是通过继承实现的

主要的实际区别可能是,在Java中,您的函数可以接受
Java.util.Collection
,而不知道它是什么类型的集合。在C++中,函数模板可以接受任何容器,但非模板函数不能接受。这影响了用户的编码风格,也被告知了——java程序员比C++程序员更容易达到运行时多态性。

从实现者的角度来看,如果你正在编写C++容器库,那么如果你觉得这有助于重用代码,那么没有任何东西可以阻止你在不同的容器之间共享私有基类。


[*]既然在C++11中保证了
size()
O(1)
,那么
empty()
很可能可以共同实现。在C++03
size()
中,仅仅“应该”是
O(1)
,并且有
std::list
的实现利用了这一点来实现
O(1)
中的
splice
的一个版本,而不是更新大小所需的
O(n)

这是一个不必要的抽象。我们有用于迭代的迭代器,特定容器的许多属性无法以有用的方式正确抽象。不要忘记可以作为模板的insert()s。值得一提的是,您可以编写独立于容器的代码,即使没有任何继承。只要给每个容器一个名称相同(比如“empty”)的方法,希望语义相同,就可以了。只有当你有虚拟方法时,你才有vtables。当您使用基类的方法时,您不需要支付任何费用。@Scharron:没错。但是,当您拥有基类时,客户端可以使用指向派生类的基类指针。在该指针上调用
delete
当然也应该执行派生类的析构函数——这意味着您需要一个虚拟析构函数。然后你有一个vtable,完全正确。使用基类的目的是“分解”事物,因此可以隐藏基类对象,并且可以未定义基类指针上的删除行为。但这很难看;-)