关于C++:你不应该继承STD::向量

Thou shalt not inherit from std::vector

好吧,这真的很难承认,但我现在确实有一种强烈的诱惑要从以东王那里继承(0)。

我需要10个定制的向量算法,我希望它们直接成为向量的成员。当然,我也希望拥有std::vector接口的其余部分。作为一个守法的公民,我的第一个想法是让一个std::vector成员在MyVector班。但是我必须手动重新提供std::vector的所有接口。太多无法输入。接下来,我考虑了私有继承,这样我就可以在公共部分编写一组using std::vector::member,而不是重新划分方法。实际上,这也很乏味。

在这里,我真的认为我可以直接从std::vector公开继承,但是在文档中提供了一个警告,这个类不应该被多态使用。我认为大多数开发人员都有足够的能力理解无论如何都不应该使用多态性。

我的决定绝对不合理吗?如果是,为什么?你能提供一个替代方案吗?这个方案会让额外的成员实际上是成员,但不会涉及重新输入向量的所有接口?我怀疑,但如果你能,我会很高兴的。

另外,除了一些白痴可以写一些像

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

使用myvector还有其他现实的危险吗?通过说"现实",我放弃了像想象一个函数,它接受一个指向向量的指针…

嗯,我已经陈述了我的情况。我犯了罪。现在由你决定是否原谅我:)


实际上,公地继承std::vector没有什么问题。如果你需要这个,就这么做。

我建议只有在必要的时候才这样做。只有当你不能用自由函数做你想做的事情时(比如应该保持某种状态)。

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

不要仅仅为了使事物看起来更好而产生新的实体。这些实体(尤其是如此普通的实体)不会生活在真空中。它们将生活在熵不断增加的混合环境中。


整个STL的设计使得算法和容器是分开的。

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

因此,我建议您接受这个约定,并以这样的方式设计您的算法:它们不关心它们正在处理的容器是什么——它们只需要一个特定类型的迭代器,它们需要这个类型的迭代器来执行它们的操作。

另外,让我把你的话说给杰夫·阿特伍德听。


不公开从std::vector继承的主要原因是缺少一个虚拟析构函数,它有效地阻止了后代的多态使用。特别是,不允许使用实际指向派生对象(即使派生类不添加成员)的deletestd::vector*,但是编译器通常不会警告您有关它的问题。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
};

正如大多数回答者所指出的,您应该首先考虑重构您的算法,以抽象它们正在操作的容器类型,并将它们保留为自由模板函数。这通常是通过使算法接受一对迭代器而不是容器作为参数来实现的。


如果你正在考虑这个问题,你显然已经杀死了办公室里的语言老师。把他们挡在一边,为什么不就这么做呢?

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

这将回避所有可能出现的错误,这些错误可能是由于意外地向上投射MyVector类而导致的,您仍然可以通过添加一点.v来访问所有的向量操作。


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

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

我强烈建议您查看标准库和头文件,并思考它们是如何工作的。


我认为很少有规则应该100%地盲目遵守。听起来你已经考虑了很多,并且确信这是一条路。所以——除非有人有明确的理由不这么做——我认为你应该继续你的计划。


没有理由从std::vector继承,除非你想使一个类的工作方式与std::vector不同,因为它以自己的方式处理std::vector定义的隐藏细节,或者除非你有思想上的理由用这个类的对象代替std::vector的对象。然而,C++标准的创建者没有提供任何EDCOX1×0的任何接口(以受保护成员的形式),这样的继承类可以利用特定的方式来改进向量。实际上,他们没有办法考虑任何可能需要扩展或微调附加实现的特定方面,因此他们不需要考虑为任何目的提供任何此类接口。

第二种选择的原因只能是意识形态上的,因为std::vector不是多态的,否则通过公共继承或公共成员身份暴露std::vector的公共接口没有区别。(假设您需要在对象中保持一些状态,这样您就无法摆脱自由函数的束缚)。从意识形态的角度来看,std::vector是一种"简单的思想",因此从意识形态上讲,不同可能阶级的对象形式的任何复杂性都没有用。


如果遵循良好的C++风格,虚拟函数的缺失不是问题,而是切片(参见HTTPS://StaskOfFult.COM/A/1446153/87329)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
//  ...
    }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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_ptrunique_ptr的形式。简而言之,不要直接从new那里得到任何东西。


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

我的类是一个稀疏的矩阵类,这意味着我需要将矩阵元素存储在某个地方,即std::vector中。我继承的原因是我有点懒惰,不想把接口写到所有的方法上,而且我正在通过swig把类与python连接起来,那里已经有了很好的std::vector接口代码。我发现将这个接口代码扩展到我的类中要比从头开始编写新的接口代码容易得多。

我能看到的唯一问题不是非虚拟析构函数的问题,而是其他一些我想重载的方法,比如push_back()resize()insert()等。私有继承确实是一个不错的选择。

谢谢!


实际上:如果在派生类中没有任何数据成员,那么就没有任何问题,甚至在多态用法中也没有。如果基类和派生类的大小不同,并且/或者您有虚拟函数(也就是v-table),则只需要一个虚拟析构函数。

但是从理论上讲,在[C++0xFCD中[Exp.Dele] ]:在第一个备选(删除对象)中,如果要删除的对象的静态类型与它的动态类型不同,则静态类型应该是要删除的对象的动态类型的基类,静态类型应该具有虚拟析构函数,或者行为是未定义的。

但是您可以从std::vector中私下派生,而不会有任何问题。我使用了以下模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
    ...


是的,只要你小心不做不安全的事情,它是安全的…我想我从来没有见过有人用新的向量,所以在实践中你可能会没事的。然而,它不是C++中常用的习语。

你能提供更多关于算法是什么的信息吗?

有时,你会因为一个设计而走上一条路,却看不到你可能走的其他路——你声称需要用10种新算法进行矢量运算,这对我来说是个警钟——有没有真正的10种矢量可以实现的通用算法,或者你是在试图制造一个既有通用功能又有V的对象呢?哪个包含特定于应用程序的函数?

我当然不是说你不应该这样做,只是你提供的信息使我觉得你的抽象可能有问题,有更好的方法来实现你想要的。


在这里,让我再介绍两种方法来满足你的需求。一种是包装std::vector的另一种方法,另一种是继承而不给用户破坏任何东西的机会:

  • 让我在不编写大量函数包装器的情况下添加另一种包装std::vector的方法。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #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)...);
        }
    };
  • 从std::SPAN继承而不是从std::vector继承,避免了dtor问题。