好吧,这真的很难承认,但我现在确实有一种强烈的诱惑要从以东王那里继承(0)。
我需要10个定制的向量算法,我希望它们直接成为向量的成员。当然,我也希望拥有std::vector接口的其余部分。作为一个守法的公民,我的第一个想法是让一个std::vector成员在MyVector班。但是我必须手动重新提供std::vector的所有接口。太多无法输入。接下来,我考虑了私有继承,这样我就可以在公共部分编写一组using std::vector::member,而不是重新划分方法。实际上,这也很乏味。
在这里,我真的认为我可以直接从std::vector公开继承,但是在文档中提供了一个警告,这个类不应该被多态使用。我认为大多数开发人员都有足够的能力理解无论如何都不应该使用多态性。
我的决定绝对不合理吗?如果是,为什么?你能提供一个替代方案吗?这个方案会让额外的成员实际上是成员,但不会涉及重新输入向量的所有接口?我怀疑,但如果你能,我会很高兴的。
另外,除了一些白痴可以写一些像
1
| std::vector<int>* p = new MyVector |
使用myvector还有其他现实的危险吗?通过说"现实",我放弃了像想象一个函数,它接受一个指向向量的指针…
嗯,我已经陈述了我的情况。我犯了罪。现在由你决定是否原谅我:)
- Re:std::vector* p = new MyVector最初并不真正起作用,因为std::vector没有虚拟析构函数。在不久的将来肯定会有未定义的行为。
- 所以,您基本上会问,是否可以根据您太懒而不能重新实现容器的接口这一事实违反一个通用规则?不,不是。你看,如果你吞下那颗苦药丸,好好地吃,你就能两全其美。别做那个人。编写健壮的代码。
- 为什么在函数参数中向向量传递指针是不现实的?
- 为什么不能/不想添加非成员函数所需的功能?对我来说,在这种情况下,这是最安全的事情。
- @crashworks:可能因为没有人动态地分配向量,所以没有函数接收到指向向量的指针会试图删除它?除此之外,参数的静态类型将是一个向量,这应该很好。
- 吉姆:EDCOX1·1接口非常庞大,当C++1X出现时,它将大大扩展。这是很多类型和更多在几年内扩展。我认为这是考虑继承而不是包容的一个很好的理由——如果一个人遵循那些函数应该是成员的前提(我对此表示怀疑)。不从STL容器派生的规则是它们不是多态的。如果你不这样使用它们,它就不适用了。
- 这个问题真正的症结在于一句话:"我希望他们是向量的直接成员"。问题中没有其他真正重要的。你为什么要这个?仅以非成员身份提供此功能有什么问题?
- 正如我在对卡尔答案的评论中提到的,缓存在非成员中很难实现。
- 对不起,我的无知,但我写的许多类没有虚拟析构函数,我从它们继承,但不使用多态性。什么是特殊的与标准容器和从他们继承?
- @拉法克:在这方面,标准集装箱没什么特别的。但是,如果您不打算以多态方式使用它,则可能会从任何类中公开继承:如果您只想添加方便方法,最好将这些方法编写为自由函数,因为这样它们也可以用于基类型的现有对象;如果您添加额外的不变量,则人们更喜欢使用D包含一个B而不是从它继承,以避免"意外的多态性"(将D对象视为B并打破这些不变量)的风险,即使它的工作更多。
- 我会直言不讳的。说:"在文档中提供一个警告,这个类不应该以多态方式使用。我认为大多数开发人员都有足够的能力理解,无论如何,这不应该被多态地使用,"这真是一厢情愿的想法。通过今天跳过几行代码,您将为将来的问题创建一个肥沃的滋生地。一眨眼之间,"其他开发人员"就会忽略文档,做他们不应该做的事情。别忘了你也在做你不该做的事。;)
- 你不应拼写错误
- @乔希:"你应该"一直比"你应该"更常见,这也是詹姆斯国王的圣经中的版本(一般来说,人们在写"你不应该"……]时都会提到这个版本)。究竟是什么让你称之为"拼写错误"?
- 其中一些参数是基于其他开发人员搞砸的,因为他们没有正确理解您的std::container固有的实现…但是如果你是唯一的开发者,并且一直期望如此,你至少可以对这些论点进行折扣,对吗?
实际上,公地继承std::vector没有什么问题。如果你需要这个,就这么做。
我建议只有在必要的时候才这样做。只有当你不能用自由函数做你想做的事情时(比如应该保持某种状态)。
问题是,MyVector是一个新的实体。这意味着一个新的C++开发人员在使用它之前应该知道它到底是什么。std::vector和MyVector有什么区别?在这里和那里哪个更好用?如果我需要把std::vector移到MyVector呢?我可以只使用swap()还是不可以?
不要仅仅为了使事物看起来更好而产生新的实体。这些实体(尤其是如此普通的实体)不会生活在真空中。它们将生活在熵不断增加的混合环境中。
- 来自我的+1,因为这是一个很好的答案,因为它提出了所有正确的问题。
- 我喜欢这个答案。它给了我一个深刻的新的处理C++类设计的选择。
- 对此,我唯一的反驳是,人们必须真正知道自己在做什么。例如,不要在MyVector中引入额外的数据成员,然后尝试将其传递给接受std::vector&或std::vector*的函数。如果使用std::vector*或std::vector&;进行任何类型的复制分配,我们会遇到切片问题,其中MyVector的新数据成员将不会被复制。通过基指针/引用调用swap也是如此。我倾向于认为任何一种有对象切片风险的继承层次结构都是不好的。
- std::vector的析构函数不是virtual的析构函数,所以你不应该继承它
- @andr&;fratelli,或者至少,如果您确实继承了它,那么您应该这样做,作为一个内部类,其他程序员在不干扰源代码的情况下无法使用(并且无意中使用了多态性),并确保您要么永远不使用多态性,要么永远不在堆上创建MyVector,要么只使用一个显式获取指向MyVector的指针或引用以删除它。不过,那将是一种糟糕的风格,所以人们需要一个非常好的理由来做这件事。
- 基本上,编码样式的存在是有原因的。如果一个人想在这种情况下违背通常被接受的风格,那么他应该:有一个非常好的理由这样做,完全意识到这样做所涉及的风险,确保他们的所有代码都考虑到这些风险,并积极防止其他人以不考虑风险的方式使用他们的代码。特恩。它通常比它的价值更麻烦,特别是因为它间接地需要比拥有一个私有的std::vector和根据需要公开它的接口更多的努力。
- 我创建了一个公开继承std::vector的类,原因是:我有一个带有非stl vector类的旧代码,我想转到stl。我将旧类重新实现为std::vector的派生类,允许我在旧代码中继续使用旧函数名(例如,count()而不是size()),同时使用std::vector函数编写新代码。我没有添加任何数据成员,因此std::vector的析构函数对于堆上创建的对象工作得很好。
整个STL的设计使得算法和容器是分开的。
这导致了不同类型迭代器的概念:常量迭代器、随机访问迭代器等。
因此,我建议您接受这个约定,并以这样的方式设计您的算法:它们不关心它们正在处理的容器是什么——它们只需要一个特定类型的迭代器,它们需要这个类型的迭代器来执行它们的操作。
另外,让我把你的话说给杰夫·阿特伍德听。
不公开从std::vector继承的主要原因是缺少一个虚拟析构函数,它有效地阻止了后代的多态使用。特别是,不允许使用实际指向派生对象(即使派生类不添加成员)的delete或std::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();
}; |
正如大多数回答者所指出的,您应该首先考虑重构您的算法,以抽象它们正在操作的容器类型,并将它们保留为自由模板函数。这通常是通过使算法接受一对迭代器而不是容器作为参数来实现的。
- IIUC,如果派生类分配了必须在销毁时释放的资源,那么缺少虚拟析构函数只是一个问题。(在多态用例中,它们不会被释放,因为在不知情的情况下,通过指向基的指针取得派生对象的所有权的上下文在时间到来时只会调用基析构函数。)其他被重写的成员函数也会出现类似的问题,因此必须注意基函数是有效的。但是如果没有额外的资源,还有其他的原因吗?
- vector的分配存储不是问题——毕竟,vector的析构函数可以通过指向vector的指针调用。只是标准禁止通过基类表达式来使用cx1〔3〕中的自由存储对象。原因当然是,(de)分配机制可能试图推断内存块的大小,以从delete的操作数中释放出来,例如,对于某些大小的对象有多个分配区域。这种限制不适用于具有静态或自动存储时间的对象的正常销毁。
- @戴维斯林,我想我们同意这一点。
- @戴维斯林啊,我明白了,你指的是我的第一条评论——评论中有一个IIUC,它以一个问题结束;我后来看到,这确实是永远禁止的。(巴塞利夫斯做了一个一般性的声明,"有效地预防",我想知道它预防的具体方法。)所以是的,我们同意:ub。
- @皮特拉.施耐德:整理评论,现在它在答案中……
- @一定是无意中发现的。固定的。
如果你正在考虑这个问题,你显然已经杀死了办公室里的语言老师。把他们挡在一边,为什么不就这么做呢?
1 2 3 4 5
| struct MyVector
{
std::vector<Thingy> v; // public!
void func1( ... ) ; // and so on
} |
这将回避所有可能出现的错误,这些错误可能是由于意外地向上投射MyVector类而导致的,您仍然可以通过添加一点.v来访问所有的向量操作。
你希望完成什么?只是提供一些功能?
C++的惯用方法是编写一些实现功能的免费函数。很可能您并不真正需要std::vector,特别是对于您正在实现的功能,这意味着您实际上通过尝试从std::vector继承而失去了可重用性。
我强烈建议您查看标准库和头文件,并思考它们是如何工作的。
- 哦,拜托,我知道,但是在这个特殊的情况下,作为成员的新功能会非常方便。
- 我不相信。你能用一些建议的代码来解释为什么吗?
- 例如,除了back()和front()之外,我还想提供middle(),我不希望它看起来像middle(v),而是v.middle()。
- @阿门:除了美学,还有什么好的理由吗?
- @斯内马尔奇:美学是一个很好的理由:)
- @Armen:更好的美学和更大的通用性,也将提供免费的front和back功能。()还考虑了在+C0+0x和Boost中的自由EDCOX1 2和EDOCX1 3的例子。
- @叔叔:嗯……我真的很喜欢你的主意,你知道吗?:)
- 我仍然不知道自由函数有什么问题。如果你不喜欢STL的"美学",也许C++对你来说是个错误的地方。添加一些成员函数并不能解决这个问题,因为许多其他算法仍然是自由函数。
- @不,美学不是一个很好的理由。代码可读性和一致性是很好的原因。但它不必看起来"漂亮"。它必须清晰易读。当然,美学是主观的。无论如何,正如建议的那样,获得一致语法的更好方法是将成员(front()、back()等)重新实现为非成员。在我的代码中,我定义了自由的begin()和end()函数,一部分是为了更一致的语法,另一部分是因为这样我就可以将它们与数组一起使用。
- @snemarch:借调。美学不是一个很好的理由。对于程序员来说,代码应该运行良好,而不是好看。
- 在外部算法中,很难缓存大量操作的结果。假设您必须计算向量中所有元素的总和,或者用向量元素作为系数来求解多项式方程。这些行动是沉重的,懒惰对他们是有用的。但是,如果不包装或继承容器,就不能引入它。
- @巴塞利夫斯的评论是我听过的第一个添加方法而不是自由函数的好理由。
- @巴塞利夫斯:在我看到你的评论之前,我几乎确信外部比内部更好。如果坚持具有外部通用功能,则必须设计缓存策略和合同:)
- @basilevs您可以使您的算法(函数)面向数据,并将容器和缓存作为输入/输出参数。这甚至会增加灵活性。(如果有人不想要缓存怎么办?然后,它们可以通过nullptr作为缓存。或者,如果有人想将其与矢量数据分开存储呢?也没问题。)
- @cubuspl42这也打开了提供默认缓存解决方案的可能性,除非用户明确指定其他解决方案,类似于标准对使用std::allocator进行分配的功能。这样做可以让程序员创建一个缓存,在大多数情况下都能以最佳方式执行,这样用户就不需要创建自己的缓存,同时允许他们在更高效的情况下使用自己的自定义缓存。
我认为很少有规则应该100%地盲目遵守。听起来你已经考虑了很多,并且确信这是一条路。所以——除非有人有明确的理由不这么做——我认为你应该继续你的计划。
- 你的第一句话是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_ptr或unique_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;
... |
- "如果基类和派生类的大小不同,或者您有虚拟函数(这意味着v表),则只需要一个虚拟析构函数。"此声明实际上是正确的,但理论上不正确。
- 是的,原则上它仍然是未定义的行为。
- 如果你声称这是未定义的行为,我想看看证据(引用标准)。
- 更具体地说:定义的行为是不能调用派生类的析构函数。因为它必须是空的,所以没有任何未定义或不好的事情发生。
- @不幸的是,Armen和Jalf在这一点上是正确的。从EDCOX1 9中的C++0x FCD:<引用>第一个备选(删除对象)中,如果要删除的对象的静态类型与它的动态类型不同,静态类型应该是要删除的对象的动态类型的基类,静态类型应该具有虚析构函数或行为未定义。
- 这很有趣,因为我实际上认为行为依赖于一个非平凡的析构函数的存在(特别是pod类可以通过指向base的指针来销毁)。
- 对堆积如山的赞美:我真的学到了一些新东西。
是的,只要你小心不做不安全的事情,它是安全的…我想我从来没有见过有人用新的向量,所以在实践中你可能会没事的。然而,它不是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问题。