好吧,这真的很难承认,但我现在确实有强烈的诱惑要从 std::vector
继承。
我需要大约 10 个自定义的向量算法,我希望它们直接成为向量的成员。但我自然也想拥有 std::vector
的其余界面。嗯,作为一个守法的公民,我的第一个想法是在 MyVector
类中有一个 std::vector
成员。但随后我将不得不手动重新提供所有 std::vector 的接口。打字太多了。接下来,我想到了私有继承,这样我就不用重新提供方法,而是在公共部分写一堆 using std::vector::member
。这其实也很乏味。
在这里,我确实认为我可以简单地从 std::vector
公开继承,但在文档中提供警告,即不应以多态方式使用此类。我认为大多数开发人员有足够的能力理解这不应该以多态方式使用。
我的决定绝对不合理吗?如果是这样,为什么?您能否提供一个替代方案,让其他成员实际上是成员,但不涉及重新键入所有向量的接口?我对此表示怀疑,但如果可以,我会很高兴。
此外,除了一些白痴可以写类似的东西之外
std::vector<int>* p = new MyVector
使用 MyVector 还有其他现实危险吗?通过说现实,我放弃了想象一个函数,它需要一个指向向量的指针......
好吧,我已经陈述了我的情况。我犯罪了。现在由你决定是否原谅我:)
std::vector
的接口挺大的,等C++1x出来的时候,会大大扩展。需要输入的内容很多,几年后需要扩展的内容更多。我认为这是考虑继承而不是包含的一个很好的理由——如果遵循这些函数应该是成员的前提(我对此表示怀疑)。不从 STL 容器派生的规则是它们不是多态的。如果您不以这种方式使用它们,则它不适用。
实际上,std::vector
的公共继承并没有错。如果你需要这个,就这样做。
我建议仅在确实有必要时才这样做。只有当你不能用自由功能做你想做的事情时(例如应该保持一些状态)。
问题是 MyVector
是一个新实体。这意味着一个新的 C++ 开发人员在使用它之前应该知道它到底是什么。 std::vector
和 MyVector
有什么区别?哪一个更适合在这里和那里使用?如果我需要将 std::vector
移动到 MyVector
怎么办?我可以只使用 swap()
吗?
不要仅仅为了使某些东西看起来更好而生产新实体。这些实体(尤其是如此常见的实体)不会生活在真空中。他们将生活在熵不断增加的混合环境中。
整个 STL 的设计方式是算法和容器是分开的。
这导致了不同类型迭代器的概念:常量迭代器、随机访问迭代器等。
因此,我建议您接受此约定并以这样的方式设计您的算法,即他们不会关心他们正在处理的容器是什么 - 他们只需要执行他们需要的特定类型的迭代器操作。
另外,让我将您重定向到 some good remarks by Jeff Attwood。
不公开继承 std::vector
的主要原因是缺少虚拟析构函数,它有效地阻止了对后代的多态使用。特别是,您是 not allowed 到 delete
的 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();
};
正如大多数回答者所指出的那样,您应该首先考虑重构您的算法以抽象它们正在操作的容器类型并将它们保留为免费的模板化函数。这通常通过使算法接受一对迭代器而不是容器作为参数来完成。
vector
的分配存储不是问题 - 毕竟,vector
的析构函数可以通过指向 vector
的指针调用。只是标准禁止通过基类表达式delete
释放存储对象。原因肯定是(取消)分配机制可能会尝试推断内存块的大小以从 delete
的操作数中释放出来,例如当有多个分配区域用于特定大小的对象时。这个限制确实不适用于具有静态或自动存储持续时间的对象的正常销毁。
如果您正在考虑这一点,那么您显然已经杀死了办公室中的语言学究。把它们排除在外,为什么不干脆做
struct MyVector
{
std::vector<Thingy> v; // public!
void func1( ... ) ; // and so on
}
这将避免意外向上转换 MyVector 类可能导致的所有错误,并且您仍然可以通过添加一点 .v
来访问所有向量操作。
你希望完成什么?只是提供一些功能?
执行此操作的 C++ 惯用方法是编写一些实现该功能的自由函数。很有可能您并不真正需要 std::vector,特别是对于您正在实现的功能,这意味着您实际上通过尝试从 std::vector 继承而失去了可重用性。
我强烈建议您查看标准库和标头,并思考它们是如何工作的。
front
和 back
功能。 :) (还要考虑 C++0x 和 boost 中免费的 begin
和 end
的示例。)
我认为很少有规则应该在 100% 的时间里盲目地遵循。听起来您已经考虑了很多,并且确信这是要走的路。所以——除非有人提出不这样做的充分的具体理由——我认为你应该继续你的计划。
没有理由从 std::vector
继承,除非想要创建一个与 std::vector
工作方式不同的类,因为它以自己的方式处理 std::vector
定义的隐藏细节,或者除非有意识形态原因使用此类的对象代替 std::vector
的对象。但是,C++ 标准的创建者没有为 std::vector
提供任何接口(以受保护成员的形式),这样继承的类可以利用这些接口以特定方式改进向量。事实上,他们没有办法考虑可能需要扩展或微调额外实现的任何特定方面,因此他们不需要考虑为任何目的提供任何此类接口。
第二种选择的原因可能只是意识形态上的,因为 std::vector
不是多态的,否则通过公共继承或通过公共成员身份公开 std::vector
的公共接口没有区别。 (假设您需要在对象中保留一些状态,这样您就无法摆脱自由函数)。在一个不太合理的音符上,从意识形态的角度来看,std::vector
似乎是一种“简单的想法”,因此任何以不同可能类别的对象形式的复杂性在意识形态上都是没有用的。
实际上:如果您的派生类中没有任何数据成员,那么您就没有任何问题,即使在多态使用方面也没有。如果基类和派生类的大小不同和/或您有虚函数(这意味着 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;
...
[expr.delete]
:<quote>在第一种选择(删除对象)中,如果待删除对象的静态类型与其动态类型不同,则静态类型应为待删除对象的动态类型的基类,静态类型应具有虚拟析构函数或行为未定义。</quote>
如果您遵循良好的 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_ptr
或 unique_ptr
的形式。简而言之,不要delete
任何不是您直接从 new
获得的东西。
是的,只要您小心不要做不安全的事情,它就是安全的……我认为我从未见过有人使用带有 new 的向量,因此在实践中您可能会没事的。但是,这不是 c++ 中的常用习语......
你能提供更多关于算法是什么的信息吗?
有时您最终会走上一条设计的道路,然后看不到您可能采取的其他路径-您声称需要使用 10 种新算法进行向量化的事实对我敲响了警钟-真的有 10 种通用目的吗?向量可以实现的算法,或者您是否正在尝试制作一个既是通用向量又包含应用程序特定功能的对象?
我当然不是说你不应该这样做,只是你给的信息敲响了警钟,这让我认为你的抽象可能有问题,有更好的方法来实现你的目标想。
我最近也从 std::vector
继承,发现它非常有用,到目前为止我还没有遇到任何问题。
我的类是一个稀疏矩阵类,这意味着我需要将我的矩阵元素存储在某个地方,即 std::vector
中。我继承的原因是我有点懒得写所有方法的接口,而且我通过 SWIG 将类连接到 Python,其中已经有用于 std::vector
的良好接口代码。我发现将这个接口代码扩展到我的类比从头开始编写一个新的更容易。
我在该方法中看到的唯一问题不是非虚拟析构函数,而是我想重载的其他一些方法,例如 push_back()
、resize()
、insert()
等。私有继承确实可能是一个不错的选择。
谢谢!
这个问题肯定会产生令人窒息的珍珠,但实际上没有正当理由避免或“不必要地增加实体”来避免从标准容器派生。最简单、最短的表达方式是最清晰、最好的。
您确实需要对任何派生类型进行所有通常的注意,但是标准中的基类的情况并没有什么特别之处。覆盖基成员函数可能会很棘手,但是对于任何非虚拟基都是不明智的,所以这里没有什么特别之处。如果要添加数据成员,如果成员必须与基的内容保持一致,则需要担心切片,但对于任何基,这同样是相同的。
我发现从标准容器派生特别有用的地方是添加一个构造函数,该构造函数精确地执行所需的初始化,而不会混淆或被其他构造函数劫持。 (我在看着你,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
在这里,让我介绍另外两种方法来做你想要的。一种是包装 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 问题。
MyVector
,然后尝试将其传递给接受std::vector&
或std::vector*
的函数。如果使用 std::vector* 或 std::vector& 涉及任何类型的复制分配,我们就会遇到无法复制MyVector
的新数据成员的切片问题。通过基指针/引用调用交换也是如此。我倾向于认为任何有对象切片风险的继承层次结构都是不好的。std::vector
的析构函数不是virtual
,因此您不应该从它继承((int)(unsigned)(int)-1) >= 0
被优化为true
以及无数其他事情时,您将继续被搞砸并且可能会生气。包括这个错误。