你不能继承std :: vector

时间:2010-12-04 11:05:46

标签: c++ oop inheritance stl vector

好的,这真的很难承认,但我现在确实有很强的诱惑从std::vector继承。

我需要大约10个自定义的矢量算法,我希望它们直接成为矢量的成员。但我自然也希望得到std::vector接口的其余部分。好吧,作为守法公民,我的第一个想法就是在std::vector班级中有MyVector成员。但是我必须手动重新提供所有std :: vector的接口。打字太多了。接下来,我考虑了私有继承,因此我会在公共部分写一堆using std::vector::member而不是重新提取方法。实际上这也很乏味。

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

我的决定绝对没有道理吗?如果是这样,为什么?你能提供一个替代方案,让其他成员实际成员,但不会涉及重新输入所有vector的界面吗?我对此表示怀疑,但如果可以,我会很高兴。

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

这样的事实
std::vector<int>* p  = new MyVector

使用MyVector时还有其他 逼真的 危险吗?通过说现实,我放弃了像想象一个指向矢量的指针的函数......

好吧,我已经陈述了我的情况。我犯罪了。现在由你来原谅我是否原谅我。)

12 个答案:

答案 0 :(得分:146)

实际上,std::vector的公共继承没有任何问题。如果你需要这个,那就去做吧。

我建议只有在真的必要时才这样做。只有你不能用自由功能做你想做的事情(例如应该保持一些状态)。

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

不要只为了让事情看起来更好而产生新的实体。这些实体(尤其是这种实体)不会生活在真空中。他们将生活在混合环境中,不断增加熵。

答案 1 :(得分:85)

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

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

因此,我建议您接受此约定并设计您的算法,使其不关心他们正在处理的容器 - 并且他们只需要特定类型他们需要执行操作的迭代器。

另外,让我将您重定向到some good remarks by Jeff Attwood

答案 2 :(得分:47)

不公开从std::vector继承的主要原因是缺少虚拟析构函数,有效地阻止了后代的多态使用。特别是,not alloweddelete 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();
};

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

答案 3 :(得分:34)

如果你正在考虑这个问题,你显然已经在办公室里杀死了语言学生。有了它们,为什么不做呢

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

这样可以回避所有可能因意外升级你的MyVector类而出现的错误,你仍然可以通过添加一点.v来访问所有的矢量操作。

答案 4 :(得分:18)

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

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

我强烈建议您查看标准库和标题,并思考它们的工作原理。

答案 5 :(得分:11)

我认为在100%的时间里应该盲目遵循很少的规则。听起来你已经给了它很多想法,并且确信这是要走的路。所以 - 除非有人提出好的具体的理由不这样做 - 我认为你应该继续你的计划。

答案 6 :(得分:6)

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

第二个选项的原因只能是意识形态的,因为std::vector s不是多态的,否则通过公共继承公开std::vector的公共接口是没有区别的。通过公共会员。 (假设您需要在对象中保留一些状态,这样您就无法使用自由函数)。从一个不那么合理的角度来看,从意识形态的角度来看,似乎std::vector是一种简单的想法&#34;,所以任何类别的对象形式的复杂性都是在意识形态上没有用处。

答案 7 :(得分:2)

实际上:如果您的派生类中没有任何数据成员,则没有任何问题,即使在多态使用中也没有。如果基类和派生类的大小不同和/或你有虚函数(这意味着一个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;
    ...

答案 8 :(得分:2)

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

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

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

谢谢!

答案 9 :(得分:1)

是的,只要你小心不做那些不安全的事情,这是安全的...我不认为我曾经见过有人使用过新的矢量,所以在实践中你可能会没事。但是,这不是c ++中常见的习语....

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

有时你最终会走上一条设计之路然后看不到你可能采取的其他路径 - 事实上你声称需要使用10个新算法来为我敲响警钟 - 真的有吗向量可以实现的10个通用算法,或者您是否试图使对象既是通用向量又包含特定于应用程序的函数?

我当然不是说你不应该这样做,只是因为你给出的信息警钟响了,这让我觉得你的抽象可能有问题,而且有更好的办法实现你想要的。

答案 10 :(得分:0)

在这里,让我介绍另外两种想要的方法。一种是包裹std::vector的另一种方法,另一种方法是在不给用户破坏任何东西的情况下继承:

  1. 让我在不编写大量函数包装器的情况下添加另一种包装std::vector的方法。
  2. #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::vector并避免出现问题。

答案 11 :(得分: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