所以我已经使用了一段时间从std::vector派生的容器。也许这是一个糟糕的设计决策,有几个原因,关于您是否应该做这样的事情的问题,我们在这里进行了广泛的讨论:
不能从std::vector继承
子类/继承标准容器?
是否有来自C++ STL容器的实际风险?
是否可以从STL容器继承实现,而不是委托?
我确信我错过了一些讨论……但是在链接中可以找到两种观点的合理论据。据我所知,"因为~向量()是非虚的"是"规则"的基础,您不应该从STL容器继承。但是,如果我查看G++4.9.2中std::vector的实现,我发现std::vector继承自_vector_base,而_vector_base是一个非虚拟析构函数。
1 2 3 4 5 6 7 8 9 10
| template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class vector : protected _Vector_base<_Tp, _Alloc>
{
...
~vector() _GLIBCXX_NOEXCEPT
{ std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish,
_M_get_Tp_allocator()); }
...
} |
哪里:
1 2 3 4 5 6 7 8 9 10
| template<typename _Tp, typename _Alloc>
struct _Vector_base
{
...
~_Vector_base() _GLIBCXX_NOEXCEPT
{ _M_deallocate(this->_M_impl._M_start, this->_M_impl._M_end_of_storage
- this->_M_impl._M_start); }
...
} |
因此,std::vector的gcc 4.9.2实现继承自具有非虚拟析构函数的基类。这使我相信这是一种可以接受的做法。为什么这样可以?这种做法不危险的具体条件是什么?
- 你可以,不管怎样,只要遗产不是公开的。
- 您的论点有几个缺陷,一个是错误的传递性,另一个是未能考虑标准库实现的黑盒性质。
- 这确实是你链接到的所有其他问题的副本。除了一个非虚拟的~vector()(如果代码中没有多态的delete),一个大问题是对象切片的潜力。vector是一种非常常用的类型,它可能有时会被复制到您不认识的代码中;如果您在自定义类中传递,则该复制将在vector上进行,并将导致对象切片。但你链接到的那些问题…
- @玉米杆是唯一区别这个问题和相关问题的东西,它是在libstdc++上竖琴,解释上票。
- @雷米亚贝尔:的确,这是唯一的区别。不幸的是,这并没有改变问题的实质。一个更好的问题是:为什么vector可以从_Vector_base继承?如果这是一个问题,那么焦点就足够不同,让这个问题独立存在。否则,它只是用稍微不同的词问同一个问题。
- @ Cornstalks。是的……我原本打算更专注于"为什么向量可以从向量库继承?"问题,但对那个问题的答案的影响有点过分了。对我来说,回到过去简化问题并更改标题是不是很糟糕?
- @我相信你说的很好,但是你能详细阐述你的"假传递性"陈述吗?我不太清楚这在这里应用意味着什么。谢谢!
- @doc07b5:您的头衔表明,我们相信,仅仅因为std::vector继承了_Vector_base,我们就可以利用这个事实来决定其他类型是否应该继承std::vector。事实上,这两者是无关的。
1:
std::vector不继承
_Vector_base的遗产
或者更确切地说,不是在我的标准库实现中。libc++实现类似矢量的操作,因此:好的。
1 2 3 4 5 6 7
| namespace std
{
template <class T, class Allocator = allocator<T> >
class vector
{
... |
当然,您的实现可能继承自基类,但我的实现没有继承。这会导致你对"规则"的第一个错误解释:好的。
C++标准库可以以多种方式实现,并且我们不能从它的一个实现中对标准库(由EDCOX1(2)定义的一个)作出广泛、彻底的假设或声明。好的。
所以,在我们进一步讨论之前,让我们记住:在这个答案中,我们不会只关注一个实现。我们将考虑所有的实现(通过关注C++标准中的定义;而不是硬盘驱动器上特定的头文件的定义)。好的。2:好吧,是的,所以std::vector继承了_Vector_base的遗产。
但在我们继续之前,让我们承认您的实现可能继承自_Vector_base,这没关系。那么,为什么允许std::vector从基类继承,但不允许您从std::vector继承?好的。3:如果你愿意的话,你可以继承std::vector(小心点)
std::vector可以从_Vector_base继承,原因与你从std::vector继承的原因相同:图书馆的实施者非常谨慎(你也应该如此)。好的。
std::vector不是具有_Vector_base指针的deleted多态性。所以我们不必担心~vector()不是虚拟的。如果您没有使用多态的std::vector指针来继承继承类delete,那么~vector()是非虚拟的就不是问题了。很好。别担心。我们不要为此大惊小怪。没有多态的delete意味着我们不必担心析构函数是否是虚拟的。好的。
但除此之外,库的实现者还确保了从使用_Vector_base引用中不分割std::vector。对你也是一样:如果你能确保你的自定义向量类永远不会被切片(由某人使用std::vector引用),那么你在这里也可以。不分片意味着我们不必担心复制品质量不好。好的。4:为什么你不应该从江户那里继承呢?
在其他问题中,这个问题已经得到了很好的回答,但我还是要在这里再次说明(并重点讨论整个_Vector_base继承问题):你(可能)不应该继承std::vector的遗产。好的。
问题是,如果这样做,使用自定义向量类的人可能会多态地删除它(他们可能有指向它的std::vector指针,而delete指针,如果这样做,这是一件糟糕的事情)。或者他们可能有一个std::vector引用您的自定义对象,并试图复制它(它将对对象进行切片,这也可能是一件坏事)(我一直假设在复制时需要对您的自定义对象的std::vector引用,因为我一直假设您足够小心,从不访问使用不小心的赋值或函数调用对对象进行计数切片;我敢肯定,您不会这么粗心,但使用std::vector引用的其他人可能是(是的,我在这里有点开玩笑)。好的。
你可以控制你用代码做什么。如果你能(小心地)控制它以确保没有多态的delete或对象切片,你就没事了。好的。
但是有时候你不能真正控制别人对你的代码做什么。如果你在一个团队中工作,如果一个团队成员无意中做了这些事情,这可能会有问题。这就是为什么我一直提出"小心"的问题。好的。
更糟糕的是,如果你的代码被客户机使用,你真的无法控制他们的行为,如果他们做了其中的一件坏事,你可能会被指责,并被责成去修复它(有趣的是,重构你以前依赖于从std::vector继承的自定义类的所有代码)(或者告诉客户他们不能这样做)。不会,但你可能会遇到一个脾气暴躁的客户,他们浪费时间调试一个他们没想到会遇到的奇怪问题)。好的。
但是,C++标准库实现者可以逃脱这种继承,因为它们可以很好地控制事物:没有人允许使用EDCOX1(0)。您可以使用std::vector。仅限于std::vector。由于您不允许(曾经)使用_Vector_base,标准库实现人员不必担心对象切片或多态delete,而且由于他们在受控环境中的实现非常小心,所以一切都很顺利。好的。
但更好的是,通过不假设std::vector是如何实现的,而将其视为(有用的)黑盒,我们可以确保使用std::vector的方式可移植到其他标准库实现中。如果您假设std::vector继承自某个基类,那么您将限制代码的可移植性。好的。好啊。
只是一个数据点。
在"C++巡游"中,Bjarne Stroustrup定义了一个类模板,该模板是从EDCOX1 22派生的。其他一切都委托给基类。
因为用户代码试图破坏_Vector_base是非法的,因为它是stdlib内部类型。这样可以防止析构函数出现任何问题。你也不能这么说。
简单地说,标准库的内部结构是一种特殊的情况,无论是在语言规则中,还是在对它们来说合理的情况下。您不能从它们对您自己的代码所做的工作中进行归纳。
答案有两部分:
因为_Vector_base知道它会被std::vector继承,所以设计成这样
每一条规则都有例外。如果继承std::vector对你来说是有意义的,那么就做有意义的事情。
规则"从不从没有虚拟析构函数的类型继承"试图解决的问题是:
- 无论何时,当你在一个没有虚拟析构函数的对象中使用delete时,被调用的析构函数并不依赖于动态类型。也就是说,如果您的delete所声明的指针类型与对象的动态类型不同,则会调用错误的析构函数。
禁止没有虚拟析构函数的类型的子类化等同于将这些类型限制为独立的类:没有父类和派生子类的类。显然,指向这些类之一的指针的声明类型永远不能与对象的动态类型不同,因此调用错误的析构函数没有危险。
然而,这条规则有点过于严格:需要避免的是调用错误的析构函数,而不是在本身或其内部进行子类化。以下是不允许调用错误析构函数的类层次结构:
1 2 3 4 5 6 7 8 9 10 11
| class Foo {
protected:
Foo();
~Foo();
};
class Bar : public Foo {
public:
Bar();
~Bar();
}; |
在这两个类中,完全可以创建一个Bar对象,并通过Foo指针或引用对其进行操作,但是,不能使用Foo*来销毁该对象—类Foo本身不可从其他代码中恢复或销毁。
现在回到std::vector<>。类std::vector<>是打算使用的类,它没有虚拟析构函数。但这并不要求std::vector<>没有基类。一个库可以按照上述Foo和Bar的模式,通过将其实现划分为两个类来自由地实现std::vector<>。唯一需要避免的是,用户使用基类指针来销毁派生对象。当然,_Vector_base类型是标准库实现的私有类型,用户不应该使用它,所以没关系。
然而,子类化std::vector<>是一个完全不同的情况:std::vector<>的析构函数是公共的,因此不能阻止类的用户。
1 2 3
| template <class T> class MyVector : public std::vector<T> {
...
}; |
要使用基类指针,他们可能会获取销毁MyVector<>。您可以使用私有继承或受保护继承来避免这个问题,但这并不能提供公共继承的好处。所以,是的,你不应该将std::vector<>划分为子类,即使std::vector<>从另一个私有类继承是完全可以的。
没有这样的规则"不要从具有非虚拟析构函数的基类继承"。如果有一个规则,它将是:"如果您甚至有一个虚拟方法,使您的析构函数为虚拟的,这也意味着不要从具有非虚拟析构函数的基类继承"。imho如果遵循此操作,可以继承stl容器。
似乎有些编译器架构师也同意这一点。字面上有一个警告,说明它-供参考:什么是'有虚拟方法…但是C++编译过程中非虚析构函数的告警意味着什么?
- 拥有虚拟方法与是否需要虚拟析构函数无关。
- 当然可以。如果没有虚拟方法,就没有多态性。如果没有多态性,就不需要虚拟析构函数。
- @不,没有。当将指针传递到基类(例如std::shared_ptr)时,您依赖它来取得指针的所有权,并在需要时正确地销毁它。当析构函数是非虚拟的时,它将只调用基析构函数,而您希望它调用派生析构函数。
- 是的,这是真的。但是,只有当您具有多态性时,使用指向基类的指针而不是实际的指针才有意义。
- 根据你所说的,C++中的每一个类都应该是一个虚拟析构函数。那么,为什么还要显式地声明它呢?让我们请求将所有析构函数都更改为虚拟语言。
- 对这个问题的全面讨论。
- @Jorenhetin,如果你把一个指向基类的指针传递给shared_ptr,你就错了。您应该将new表达式的结果直接传递给shared_ptr(或使用make_shared),然后它保证正确地销毁它,即使稍后转换为shared_ptr,或者即使您执行shared_ptr(new derived)。那么就不需要虚拟析构函数了。
- @LightnessRaceSinorbit,不,你的第一个评论是正确的。注意,没有shared_ptr::shared_ptr(T*)构造函数,只有template shared_ptr::shared_ptr(Y* p)构造函数,它"拥有指针p",即它将称为delete p而不是delete (T*)p。
- @乔纳森韦克利:啊,废话。
- @lightnessracesinorbit,这是共享指针设计的一个非常有意的部分,请参见boost.org/doc/libs/1_57_0/libs/smart_ptr/&hellip;[此构造函数是一个模板,用于记住传递的实际指针类型。析构函数将使用相同的指针调用delete,并使用其原始类型完成,即使T没有虚拟析构函数,或者是void时也是如此。
- "GSF"让我们改变语言,所有析构函数都是虚拟的。"C++中的虚拟Dops之所以被选中是因为它带来了一些(虽然很小)开销,并不总是必要的。C++的趋势是高级特征可以选择,而不是选择退出。
- @毛绒的谢谢。我所说的与乔尔恩海汀所说的相矛盾,实际上我不是那个意思。