好吧,这真的很难承认,但我确实有一个强烈的诱惑,从std::vector继承。

I need about 10 customized algorithms for vector and I want them to be directly members of the vector. But naturally I want also to have the rest of std::vector's interface. Well, my first idea, as a law-abiding citizen, was to have an std::vector member in MyVector class. But then I would have to manually reprovide all of the std::vector's interface. Too much to type. Next, I thought about private inheritance, so that instead of reproviding methods I would write a bunch of using std::vector::member's in the public section. This is tedious too actually.

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

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

而且,除了有些白痴能写出这样的东西

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

使用MyVector还有其他现实的危险吗?通过说现实,我放弃了一些东西,比如想象一个函数,它接受一个指向向量的指针…

我已经陈述了我的观点。我有罪。现在就看你是否原谅我了:)


当前回答

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

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

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的形式获取它。简而言之,不要删除不是直接从new中获取的任何内容。

其他回答

这个问题肯定会让人紧张得喘不过气来,但事实上,没有理由避免从标准容器派生,或者“不必要地增加实体”。最简单、最短的表达是最清晰、最好的。

您确实需要对任何派生类型进行所有通常的注意,但对于来自标准的基类型的情况并没有什么特别之处。重写base成员函数可能很棘手,但对于任何非虚基来说都是不明智的,因此这里没有太多特别之处。如果要添加一个数据成员,如果该成员必须与基库的内容保持一致,则需要考虑切片问题,但这同样适用于任何基库。

The place where I have found deriving from a standard container particularly useful is to add a single constructor that does precisely the initialization needed, with no chance of confusion or hijacking by other constructors. (I'm looking at you, initialization_list constructors!) Then, you can freely use the resulting object, sliced -- pass it by reference to something expecting the base, move from it to an instance of the base, what have you. There are no edge cases to worry about, unless it would bother you to bind a template argument to the derived class.

在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()。

(The reason that reserve_in, technically, needs to wait for C++20 is that prior Standards don't require the capacity of an empty vector to be preserved across moves. That is acknowledged as an oversight, and can reasonably be expected to be fixed as a defect in time for '20. We can also expect the fix to be, effectively, backdated to previous Standards, because all existing implementations actually do preserve capacity across moves; the Standards just haven't required it. The eager can safely jump the gun -- reserving is almost always just an optimization anyway.)

有些人会认为,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

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

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

有时你在设计上走了一条路,然后看不到你可能走的其他道路——你声称需要用10种新算法来做矢量,这一事实给我敲响了警钟——矢量真的可以实现10种通用算法吗?或者你试图做一个对象,它既是通用矢量,又包含特定的应用程序功能?

我当然不是说你不应该这样做,只是你所提供的信息敲响了警钟,这让我认为也许你的抽象是有问题的,有更好的方法来实现你想要的。

实际上:如果派生类中没有任何数据成员,就不会有任何问题,甚至在多态使用中也不会。只有在基类和派生类的大小不同和/或有虚函数(即v-table)时,才需要虚析构函数。

但在理论上:来自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;
    ...

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

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

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

谢谢!

在这里,让我再介绍两种做你想做的事的方法。一种是包装std::vector的另一种方法,另一种是继承而不给用户破坏任何东西的方法:

让我添加另一种包装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)...); } }; Inheriting from std::span instead of std::vector and avoid the dtor problem.