您不应该继承std :: vector


189

好的,这确实很难让人承认,但是目前我确实有很强的诱惑力可以继承std::vector

我需要大约10种针对矢量的自定义算法,并且希望它们直接成为矢量的成员。但是我自然也希望拥有的其余std::vector界面。好吧,作为守法公民,我的第一个想法是std::vectorMyVector班级成员。但是然后我将不得不手动重新提供所有std :: vector的接口。输入太多。接下来,我考虑了私有继承,因此我将using std::vector::member在公共部分写一堆,而不是提供方法。其实这也很乏味。

在这里,我确实确实认为我可以简单地从公开继承std::vector,但是在文档中提供警告,该类不应被多态使用。我认为大多数开发人员都有足够的能力来理解,无论如何都不应多态地使用它。

我的决定绝对不合理吗?如果是这样,为什么?您能否提供一种替代方法,使其他成员实际成为成员,而不涉及重新键入vector接口的所有内容?我对此表示怀疑,但如果可以的话,我会很高兴。

此外,除了一些白痴可以写类似的事实

std::vector<int>* p  = new MyVector

使用MyVector 还有其他现实的危险吗?通过说现实,我舍弃了像想象一个需要指向矢量指针的函数之类的事情。

好吧,我已经说了我的案子。我犯罪了。现在由你来原谅我:)


9
因此,您基本上是基于您懒于重新实现容器的接口这一事实,问是否可以违反一条通用规则?那不,不是。看,如果您吞下那颗苦药并正确地做,那么您可以两全其美。别那样 编写健壮的代码。
吉姆·布里索

7
为什么/不希望通过非成员函数添加所需的功能?对我来说,这是在这种情况下最安全的事情。
西蒙妮2010年

11
@Jim:std::vector的接口非常大,当C ++ 1x出现时,它将大大扩展。在未来几年中,要键入的内容很多,而且还有更多内容需要扩展。我认为这是考虑继承而不是包含的一个很好的理由-如果有人遵循那些功能应该是成员的前提(我对此表示怀疑)。不衍生自STL容器的规则是它们不是多态的。如果您不以这种方式使用它们,则不适用。
2010年

9
问题的实质是一句话:“我希望他们直接成为媒介的成员”。问题中的其他内容都没有任何关系。你为什么要“想要”这个?仅以非成员身份提供此功能有什么问题?
jalf

8
@JoshC:“你应该”总是比“你应该”更普遍,它也是《詹姆士国王圣经》中发现的版本(人们通常在写“你不应该[...] ”)。到底什么会导致您称其为“拼写错误”?
ruakh 2014年

Answers:


155

实际上,公共继承没有错 std::vector。如果需要此功能,请执行此操作。

我建议只有在确实必要时。仅当您无法使用自由函数做您想做的事情时(例如,应保持某种状态)。

问题在于这MyVector是一个新实体。这意味着新的C ++开发人员在使用它之前应该知道它到底是什么。std::vector和之间有什么区别MyVector?在这里和那里使用哪个更好?如果我需要搬到std::vectorMyVector怎么办?我可以用吗swap()还是不?

不要仅仅为了使外观变得更好而产生新的实体。这些实体(尤其是这样的实体)不会存在真空中。它们将生活在熵不断增加的混合环境中。


7
我对此的唯一反对意见是,一个人必须真正知道他在做什么。例如,请勿将其他数据成员引入MyVector,然后尝试将其传递给接受std::vector&或的函数std::vector*。如果使用std :: vector *或std :: vector&涉及任何类型的复制分配,我们就会遇到切片问题,其中的新数据成员MyVector将不会被复制。通过基本指针/引用调用swap也是一样。我倾向于认为任何可能导致对象切片的继承层次结构都是不好的。
stinky472'9

13
std::vector的析构函数是不是virtual,所以你永远不应该从它继承
安德烈FRATELLI

2
由于以下原因,我创建了一个公开继承std :: vector的类:我有一个带有非STL矢量类的旧代码,并且我想转到STL。我将旧类重新实现为std :: vector的派生类,使我可以继续使用旧代码中的旧函数名称(例如Count()而不是size()),同时使用std :: vector编写新代码功能。我没有添加任何数据成员,因此std :: vector的析构函数对于在堆上创建的对象可以正常工作。
格雷厄姆·阿舍

3
@GrahamAsher如果通过指向基础的指针删除对象,并且析构函数不是虚拟的,则您的程序表现出未定义的行为。未定义行为的一种可能结果是“在我的测试中效果很好”。另一个是它通过电子邮件将您的祖母浏览历史发送给您的祖母。两者都符合C ++标准。随着编译器,操作系统的点发布,它从一种改变为另一种,或者也符合月相。
Yakk-Adam Nevraumont

2
@GrahamAsher否,每当您通过指向基的指针删除任何对象而没有虚拟析构函数时,根据标准,这是未定义的行为。我了解您的想法。你只是碰巧是错的。这种未定义行为的一种可能的症状(也是最常见的一种现象)是“调用了基类析构函数并且它起作用”,因为这是编译器通常生成的幼稚机器代码。这并不安全,也不是一个好主意。
Yakk-Adam Nevraumont

92

整个STL的设计方式使算法和容器是分开的

这导致了不同类型的迭代器的概念:常量迭代器,随机访问迭代器等。

因此,我建议您接受此约定并以这样的方式设计算法,使他们不必关心正在使用的容器是什么并且它们只需要执行其操作所需的特定类型的迭代器即可。操作。

另外,让我将您引向Jeff Attwood的一些评价


63

std::vector公开继承的主要原因是缺少虚拟析构函数,该析构函数有效地防止了您对子代进行多态使用。特别是,你都不准delete一个std::vector<T>*在派生类对象(即使在派生类中没有增加成员),实际点,但编译器一般不能向您发出警告。

在这些条件下允许私有继承。因此,我建议使用私有继承并从父级转发必需的方法,如下所示。

class AdVector: private std::vector<double>
{
    typedef double T;
    typedef std::vector<double> vector;
public:
    using vector::push_back;
    using vector::operator[];
    using vector::begin;
    using vector::end;
    AdVector operator*(const AdVector & ) const;
    AdVector operator+(const AdVector & ) const;
    AdVector();
    virtual ~AdVector();
};

正如大多数答复者所指出的那样,您应该首先考虑重构算法以抽象化它们正在操作的容器的类型,并将其保留为免费的模板化函数。通常通过使算法接受一对迭代器而不是容器作为参数来完成此操作。


IIUC,缺少虚拟析构函数只是一个问题,如果派生类分配了必须在销毁时释放的资源。(在多态用例中它们不会被释放,因为在不知不觉中通过指向base的指针获取派生对象的所有权的上下文只会在适当的时候调用base析构函数。)类似的问题也来自于其他重写的成员函数,因此必须谨慎认为基本电话有效。但是如果没有额外的资源,还有其他原因吗?
彼得-恢复莫妮卡

2
vector分配的存储不是问题-毕竟,vector可以通过指向的指针来调用析构函数vector。这只是标准者禁用delete荷兰国际集团免费存储对象通过一个基类的表达。原因肯定是,(解除)分配机制可能会尝试推断内存块的大小以使其不包含delete操作数,例如,对于某些大小的对象有多个分配区域时。从本质上讲,此限制不适用于具有静态或自动存储持续时间的对象的正常破坏。
彼得-恢复莫妮卡

@DavisHerring我想我们同意:-)。
彼得-恢复莫妮卡

@DavisHerring啊,我明白了,您指的是我的第一条评论-该评论中有一个IIUC,并以一个问题结尾。后来我看到确实禁止这样做。(Basilevs做了一个一般性声明,“有效地阻止”,我想知道它可以阻止的具体方式。)是的,我们同意:UB。
彼得-恢复莫妮卡

@Basilevs一定是无意中。固定。
ThomasMcLeod

36

如果您正在考虑这一点,那么您显然已经杀害了办公室中的语言学习者。把他们挡在门外,为什么不做

struct MyVector
{
   std::vector<Thingy> v;  // public!
   void func1( ... ) ; // and so on
}

这将避免因意外上载MyVector类而引起的所有可能的错误,并且您仍然可以通过添加一点来访问所有矢量操作.v


并公开容器和算法?请参阅上面的Kos答案。
bruno nery12年


19

您希望完成什么?只是提供一些功能?

C ++惯用的方法是编写一些实现该功能的免费函数。有可能您实际上并不需要std :: vector,特别是对于所实现的功能,这意味着您实际上试图通过从std :: vector继承而失去了可重用性。

我强烈建议您查看标准库和标头,并沉思它们的工作方式。


5
我不相信。您能否用一些建议的代码进行更新以解释原因?
Karl Knechtel '12

6
@Armen:除了美学之外,还有什么好的理由吗?
2010年

12
@Armen:更好的美学和更大的通用性也将是提供自由frontback功能。:)(也考虑免费的示例begin以及endC ++ 0x和boost中的示例。)
UncleBens 2010年

3
我仍然没有自由函数的问题。如果您不喜欢STL的“美学”,那么从美学上讲,C ++对您来说是错误的地方。并且添加一些成员函数不会解决它,因为许多其他算法仍然是自由函数。
Frank Osterfeld 2010年

17
很难将繁重的运算结果缓存在外部算法中。假设您必须计算向量中所有元素的总和或以向量元素为系数来求解多项式方程。这些操作很繁琐,懒惰对他们很有用。但是,如果不包装或从容器继承就不能引入它。
Basilevs 2010年

14

我认为很少有100%的时间应该盲目遵循规则。听起来您已经考虑了很多,并确信这是要走的路。所以-除非有人提出了明确的特定理由不这样做,否则我认为您应该继续执行您的计划。


9
您的第一句话在100%的时间内都是正确的。:)
史蒂夫·费洛斯

5
不幸的是,第二句话不是。他没有考虑太多。大多数问题是无关紧要的。唯一显示出他动机的部分是“我希望他们直接成为媒介的成员”。我想要。没有理由为什么这是可取的。听起来他根本没有考虑
jalf

7

没有理由要继承,std::vector除非有人希望创建一个类,其工作方式不同于std::vector,因为它以自己的方式处理std::vector的定义的隐藏细节,或者除非出于意识形态的原因要使用此类的对象代替std::vector的。但是,C ++标准的创建者没有提供std::vector任何接口(以受保护成员的形式),此类继承的类可以利用该接口来以特定方式改进向量。实际上,他们没有办法考虑可能需要扩展或微调其他实现的任何特定方面,因此他们无需考虑出于任何目的提供任何此类接口。

第二种选择的原因仅仅是出于意识形态,因为std::vectors不是多态的,否则,无论是std::vector通过公共继承还是通过公共成员身份公开的公共接口,都没有区别。(假设您需要在对象中保留一些状态,以使您无法摆脱自由功能)。从意识形态的角度来看,从一个不太合理的角度来看,看来std::vectors是一种“简单的想法”,因此,在意识形态上不同可能类别的对象形式的任何复杂性在意识形态上都没有用。


好答案。欢迎来到SO!
Armen Tsirunyan 2014年

4

实际上:如果您的派生类中没有任何数据成员,则不会有任何问题,甚至在多态用法上也不会有问题。仅当基类和派生类的大小不同和/或具有虚拟函数(即v表)时,才需要虚拟析构函数。

但从理论上讲:从C ++ 0x FCD中的[expr.delete]:在第一个替代方案(删除对象)中,如果要删除的对象的静态类型不同于其动态类型,则静态类型应为要删除的对象的动态类型和静态类型的基类应具有虚拟析构函数或行为未定义。

但是您可以私自从std :: vector派生出任何问题。我使用了以下模式:

class PointVector : private std::vector<PointType>
{
    typedef std::vector<PointType> Vector;
    ...
    using Vector::at;
    using Vector::clear;
    using Vector::iterator;
    using Vector::const_iterator;
    using Vector::begin;
    using Vector::end;
    using Vector::cbegin;
    using Vector::cend;
    using Vector::crbegin;
    using Vector::crend;
    using Vector::empty;
    using Vector::size;
    using Vector::reserve;
    using Vector::operator[];
    using Vector::assign;
    using Vector::insert;
    using Vector::erase;
    using Vector::front;
    using Vector::back;
    using Vector::push_back;
    using Vector::pop_back;
    using Vector::resize;
    ...

3
“如果基类和派生类的大小不相同,或者您具有虚函数(即v表),则仅需要虚拟析构函数。” 这种说法实际上是正确的,但从理论上讲是不正确的
Armen Tsirunyan 2010年

2
是的,原则上它仍然是未定义的行为。
jalf

如果您声称这是未定义的行为,我希望看到一个证明(引用标准)。
hmuelner

8
@hmuelner:不幸的是,Armen和jalf在这一点上是正确的。从[expr.delete]C ++ 0x FCD中开始:<quote>在第一个替代方案(删除对象)中,如果要删除的对象的静态类型不同于其动态类型,则静态类型应为动态类型的基类。要删除的对象的类型,静态类型应具有虚拟析构函数或行为未定义。</ quote>
Ben Voigt 2010年

1
这很有趣,因为我实际上认为行为取决于非平凡的析构函数的存在(具体来说,可以通过指向基址的指针销毁POD类)。
Ben Voigt 2010年

3

如果遵循良好的C ++风格,则缺少虚函数不是问题,而是切片(请参见 https://stackoverflow.com/a/14461532/877329

为什么没有虚拟功能不是问题?因为函数不应该尝试获取delete它的任何指针,因为它没有它的所有权。因此,如果遵循严格的所有权策略,则不需要虚拟析构函数。例如,这总是错误的(使用或不使用虚拟析构函数):

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    delete obj;
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj); //Will crash here. But caller does not know that
//  ...
    }

相反,这将始终有效(带有或不带有虚拟析构函数):

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj);
//  The correct destructor *will* be called here.
    }

如果对象是由工厂创建的,则工厂还应返回指向工作删除器的指针,该指针应代替delete,因为工厂可以使用自己的堆。呼叫者可以使用share_ptr或形式获取它 unique_ptr。简而言之,不要直接从中delete得到任何东西。new


2

是的,只要您小心不要做不安全的事情,它就是安全的。我认为我从未见过有人使用带有new的向量,因此在实践中您会没事的。但是,这不是c ++中的常见用法。

您能否提供有关算法是什么的更多信息?

有时,您最终在设计上走了一条路,却看不到您可能走过的另一条路-您声称需要使用10种新算法进行导航的事实为我敲响了警钟-实际上有10个通用用途向量可以实现的算法,或者您是否试图创建既是通用向量又包含特定于应用程序功能的对象?

我当然不是在说您不应该这样做,只是因为您所提供的信息已经响起了警钟,这让我认为您的抽象也许出了点问题,并且有更好的方法来实现您的目标想。


2

我也是从std::vector最近继承过来的,并发现它非常有用,到目前为止,我还没有遇到任何问题。

我的班级是一个稀疏的矩阵类,这意味着我需要将矩阵元素存储在某个地方,即 std::vector。我之所以继承,是因为我太懒了,无法为所有方法编写接口,而且我正在通过SWIG将类与Python接口,那里已经有很好的接口代码std::vector。我发现将这个接口代码扩展到我的类要容易得多,而不是从头开始编写新的代码。

我可以用这种方法看到的唯一的问题是没有这么多与非虚析构函数,而是一些其他的方法,我想超载,如push_back()resize()insert()等私有继承确实可以成为一个不错的选择。

谢谢!


10
在我的经验中,最严重的长期的损害往往是由人谁尝试一些不明智引起的,“ 到目前为止,还没有经历过(读预告)与它的任何问题”。
幻灭了

0

在这里,让我介绍两种您想要的方式。一种是包装的std::vector另一种方式,另一种是不给用户机会破坏任何东西的继承方式:

  1. 让我添加另一种包装方式,std::vector而无需编写很多函数包装器。

#include <utility> // For std:: forward
struct Derived: protected std::vector<T> {
    // Anything...
    using underlying_t = std::vector<T>;

    auto* get_underlying() noexcept
    {
        return static_cast<underlying_t*>(this);
    }
    auto* get_underlying() const noexcept
    {
        return static_cast<underlying_t*>(this);
    }

    template <class Ret, class ...Args>
    auto apply_to_underlying_class(Ret (*underlying_t::member_f)(Args...), Args &&...args)
    {
        return (get_underlying()->*member_f)(std::forward<Args>(args)...);
    }
};
  1. 继承自std :: span而不是std::vectordtor问题。

0

这个问题肯定会引起喘不过气来,但实际上并没有正当理由避免或“不必要地增加实体”来避免从标准容器中派生。最简单,最短的表达方式是最清晰,最好的。

您确实需要对所有派生类型进行所有常规维护,但是对于标准的基本情况并没有什么特别的。覆盖基本成员函数可能很棘手,但是对任何非虚拟基本成员来说这都是不明智的,因此这里没有太多特殊之处。如果要添加数据成员,则必须担心切片,如果该成员必须与基本内容保持一致,但是对于任何基本而言,这也是相同的。

我发现从标准容器派生的地方特别有用,是添加一个可以精确执行所需初始化的构造函数,而不会引起其他构造函数的混乱或劫持。(我正在看你的initialize_list构造函数!)然后,您可以自由地使用切片后的结果对象-通过引用将其传递给需要基础的对象,然后从该对象移至基础的实例,您拥有什么。无需担心任何极端情况,除非它会打扰您将模板参数绑定到派生类。

在C ++ 20中将立即使用该技术的地方是保留。我们可能写过的地方

  std::vector<T> names; names.reserve(1000);

我们可以说

  template<typename C> 
  struct reserve_in : C { 
    reserve_in(std::size_t n) { this->reserve(n); }
  };

然后甚至有作为班级成员的

  . . .
  reserve_in<std::vector<T>> taken_names{1000};  // 1
  std::vector<T> given_names{reserve_in<std::vector<T>>{1000}}; // 2
  . . .

(根据偏好设置),而无需编写构造函数仅用于在其上调用reserve()。

(从reserve_in技术上来说,需要等待C ++ 20的原因是,先前的标准不需要跨步保留空向量的容量。这被认为是疏忽,可以合理地预期会得到修复。作为20年代及时的缺陷。我们还可以期望该修补程序可以有效地追溯到以前的标准,因为所有现有的实现实际上都保留了跨移动的能力;这些标准并不需要它。枪-无论如何,保留几乎总是一种优化。)

有人认为reserve_in自由函数模板可以更好地解决的情况:

  template<typename C> 
  auto reserve_in(std::size_t n) { C c; c.reserve(n); return c; }

这样的选择当然是可行的-由于* RVO,有时甚至可能无限快。但是,应根据自身的优点选择派生函数或自由函数,而不是出于对标准组件派生的毫无根据的迷惑(嘿!)。在上面的示例用法中,只有第二种形式可以与free函数一起使用;尽管可以在类上下文之外写得更简洁一些:

  auto given_names{reserve_in<std::vector<T>>(1000)}; // 2
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.