ChatGPT解决这个技术问题 Extra ChatGPT

你不应该从 std::vector 继承

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

我需要大约 10 个自定义的向量算法,我希望它们直接成为向量的成员。但我自然也想拥有 std::vector 的其余界面。嗯,作为一个守法的公民,我的第一个想法是在 MyVector 类中有一个 std::vector 成员。但随后我将不得不手动重新提供所有 std::vector 的接口。打字太多了。接下来,我想到了私有继承,这样我就不用重新提供方法,而是在公共部分写一堆 using std::vector::member。这其实也很乏味。

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

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

此外,除了一些白痴可以写类似的东西之外

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

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

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

因此,您基本上是基于您懒得重新实现容器接口这一事实而询问是否可以违反通用规则?那么不,不是。看,如果你吞下那颗苦药并正确地做,你可以两全其美。不要成为那个人。编写健壮的代码。
为什么你不能/不想用非成员函数添加你需要的功能?对我来说,在这种情况下,这将是最安全的做法。
@Jim:std::vector的接口挺大的,等C++1x出来的时候,会大大扩展。需要输入的内容很多,几年后需要扩展的内容更多。我认为这是考虑继承而不是包含的一个很好的理由——如果遵循这些函数应该是成员的前提(我对此表示怀疑)。不从 STL 容器派生的规则是它们不是多态的。如果您不以这种方式使用它们,则它不适用。
问题的真正实质在于一句话:“我希望他们直接成为向量的成员”。问题中的其他任何内容都不重要。你为什么“想要”这个?仅以非会员身份提供此功能有什么问题?
@JoshC:“你应该”一直比“你应该”更常见,它也是詹姆士国王圣经中的版本(这通常是人们在写“你不应该 [...] ”)。到底是什么让你称之为“拼写错误”?

T
ThomasMcLeod

实际上,std::vector 的公共继承并没有错。如果你需要这个,就这样做。

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

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

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


我对此的唯一反驳是,一个人必须真正知道他在做什么才能做到这一点。例如,不要将其他数据成员引入 MyVector,然后尝试将其传递给接受 std::vector&std::vector* 的函数。如果使用 std::vector* 或 std::vector& 涉及任何类型的复制分配,我们就会遇到无法复制 MyVector 的新数据成员的切片问题。通过基指针/引用调用交换也是如此。我倾向于认为任何有对象切片风险的继承层次结构都是不好的。
std::vector 的析构函数不是 virtual,因此您不应该从它继承
出于这个原因,我创建了一个公开继承 std::vector 的类:我有一个带有非 STL 向量类的旧代码,我想迁移到 STL。我将旧类重新实现为 std::vector 的派生类,允许我在旧代码中继续使用旧函数名称(例如,Count() 而不是 size()),同时使用 std::vector 编写新代码功能。我没有添加任何数据成员,因此 std::vector 的析构函数对于在堆上创建的对象工作得很好。
@GrahamAsher 不,每当您通过指向没有虚拟析构函数的 base 指针删除任何对象时,这都是标准下的未定义行为。我理解你的想法;你只是碰巧错了。 “基类析构函数被调用,并且它工作”是这种未定义行为的一种可能症状(也是最常见的),因为这是编译器通常生成的幼稚机器代码。这并不安全,也不是一个好主意。
@graham C++ 不是由它生成的汇编代码定义的。标准清晰、完整,定义规范;它定义了什么是 C++。如果您想更改标准,请提出建议。在那之前,您的代码的行为明确且明确地没有被标准定义。我得到它。认为 C++ 是由它生成的代码定义的,这是一个常见的错误。但是,在您理解这个根本错误之前,当 ((int)(unsigned)(int)-1) >= 0 被优化为 true 以及无数其他事情时,您将继续被搞砸并且可能会生气。包括这个错误。
K
Kos

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

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

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

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


T
ThomasMcLeod

不公开继承 std::vector 的主要原因是缺少虚拟析构函数,它有效地阻止了对后代的多态使用。特别是,您是 not alloweddeletestd::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,只有在派生类分配了必须在销毁时释放的资源时,虚拟析构函数的缺失才是问题。 (它们不会在多态用例中被释放,因为上下文通过指向基的指针在不知情的情况下获取派生对象的所有权,只会在适当的时候调用基析构函数。)其他重写的成员函数也会出现类似的问题,因此必须小心可以认为基本的调用是有效的。但是没有额外的资源,还有其他原因吗?
vector 的分配存储不是问题 - 毕竟,vector 的析构函数可以通过指向 vector 的指针调用。只是标准禁止通过基类表达式delete释放存储对象。原因肯定是(取消)分配机制可能会尝试推断内存块的大小以从 delete 的操作数中释放出来,例如当有多个分配区域用于特定大小的对象时。这个限制确实不适用于具有静态或自动存储持续时间的对象的正常销毁。
@DavisHerring 我认为我们同意:-)。
@DavisHerring 啊,我明白了,你指的是我的第一条评论——那条评论中有一个 IIUC,它以一个问题结束;后来我看到确实它总是被禁止的。 (Basilevs 做了一般性声明,“有效防止”,我想知道它防止的具体方式。)所以是的,我们同意:UB。
@Basilevs 那一定是无意的。固定的。
C
Crashworks

如果您正在考虑这一点,那么您显然已经杀死了办公室中的语言学究。把它们排除在外,为什么不干脆做

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

这将避免意外向上转换 MyVector 类可能导致的所有错误,并且您仍然可以通过添加一点 .v 来访问所有向量操作。


并暴露容器和算法?见上面科斯的回答。
K
Karl Knechtel

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

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

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


我不相信。您能否更新一些建议的代码来解释原因?
@Armen:除了美学,还有什么好的理由吗?
@Armen:更好的美学和更大的通用性,也将提供免费的 frontback 功能。 :) (还要考虑 C++0x 和 boost 中免费的 beginend 的示例。)
我仍然不知道免费功能有什么问题。如果您不喜欢 STL 的“美学”,那么从美学角度来说,C++ 可能不适合您。添加一些成员函数并不能解决它,因为其他算法仍然是自由函数。
在外部算法中很难缓存大量操作的结果。假设您必须计算向量中所有元素的总和,或求解一个以向量元素为系数的多项式方程。这些操作很繁重,懒惰对他们很有用。但是你不能在不包装或继承容器的情况下引入它。
N
NPE

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


你的第一句话百分百正确。 :)
不幸的是,第二句话不是。他没有多想。大多数问题是无关紧要的。唯一显示他动机的部分是“我希望他们直接成为向量的成员”。我想。没有理由为什么这是可取的。这听起来像是他根本没有考虑过。
E
Evgeniy

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

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


很好的答案。欢迎来到 SO!
h
hmuelner

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

“如果基类和派生类的大小不同/或者你有虚函数(这意味着一个 v-table),你只需要一个虚拟析构函数。”这种说法实际上是正确的,但在理论上是不正确的
是的,原则上它仍然是未定义的行为。
如果您声称这是未定义的行为,我希望看到一个证明(来自标准的引用)。
@hmuelner:不幸的是,Armen 和 jalf 在这一点上是正确的。来自 C++0x FCD 中的 [expr.delete]:<quote>在第一种选择(删除对象)中,如果待删除对象的静态类型与其动态类型不同,则静态类型应为待删除对象的动态类型的基类,静态类型应具有虚拟析构函数或行为未定义。</quote>
这很有趣,因为我实际上认为这种行为取决于一个重要的析构函数的存在(特别是,POD 类可以通过指向基的指针来销毁)。
C
Community

如果您遵循良好的 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_ptrunique_ptr 的形式。简而言之,不要delete 任何不是您直接new 获得的东西。


j
jcoder

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

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

有时您最终会走上一条设计的道路,然后看不到您可能采取的其他路径-您声称需要使用 10 种新算法进行向量化的事实对我敲响了警钟-真的有 10 种通用目的吗?向量可以实现的算法,或者您是否正在尝试制作一个既是通用向量又包含应用程序特定功能的对象?

我当然不是说你不应该这样做,只是你给的信息敲响了警钟,这让我认为你的抽象可能有问题,有更好的方法来实现你的目标想。


M
Mad Physicist

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

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

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

谢谢!


根据我的经验,最严重的长期损害通常是由尝试不明智的事情的人造成的,并且“到目前为止还没有遇到(阅读注意到)任何问题”。
N
Nathan Myers

这个问题肯定会产生令人窒息的珍珠,但实际上没有正当理由避免或“不必要地增加实体”来避免从标准容器派生。最简单、最短的表达方式是最清晰、最好的。

您确实需要对任何派生类型进行所有通常的注意,但是标准中的基类的情况并没有什么特别之处。覆盖基成员函数可能会很棘手,但是对于任何非虚拟基都是不明智的,所以这里没有什么特别之处。如果要添加数据成员,如果成员必须与基的内容保持一致,则需要担心切片,但对于任何基,这同样是相同的。

我发现从标准容器派生特别有用的地方是添加一个构造函数,该构造函数精确地执行所需的初始化,而不会混淆或被其他构造函数劫持。 (我在看着你,initialization_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

J
JiaHao Xu

在这里,让我介绍另外两种方法来做你想要的。一种是包装 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)...);
    }
};

从 std::span 而不是 std::vector 继承并避免 dtor 问题。