【问题标题】:std::unique_ptr<T[]> with an array of derived objects, use of deleted functionstd::unique_ptr<T[]> 带有派生对象数组,使用已删除函数
【发布时间】:2016-02-01 09:33:20
【问题描述】:

在我的数值物理代码中,我需要使用unique_ptr 创建一个派生对象数组,它们的类型是基类。通常,我会:

// Header file of the Base class
class Particle{
public:
    Particle();             // some constructor
    virtual ~Particle();    // virtual destructor because of polymorphism
    virtual function();     // some random function for demonstration
};

// Header file of the Derived class
class Electron : public Particle{
public:
    Electron();
    // additional things, dynamic_cast<>s, whatever
};

稍后在我的代码中,要使用 Base 类型指针创建一个 Derived 对象数组,我会这样做

Particle* electrons = new Electron[count];

优点是我能够以非常方便的方式使用数组electrons[number].function(),因为[] 中的增量值实际上是指向对象@987654327 的正确实例的内存地址@ 在数组中。但是,使用原始指针会很麻烦,所以我决定使用智能指针。

问题在于派生对象的定义。我可以做到以下几点:

std::unique_ptr<Particle, std::default_delete<Particle[]>> electrons(new Electron[count]);

它创建了多态电子数组,甚至使用了delete[] 的正确调用。问题在于调用数组的具体对象的方式,因为我必须这样做:

electrons.get()[number].function();

我不喜欢get() 部分,一点也不喜欢。

我可以做到以下几点:

std::unique_ptr<Particle[]> particles(new Particle[count]);

是的,在数组中调用Particle类型的实例

particles[number].function();

一切都会好起来的,除了我没有使用Electron类的具体细节的部分,因此代码没用。

现在有趣的部分,让我们再做一件事,好吗?

std::unique_ptr<Particle[]> electrons(new Electron[count]);

轰隆隆!

use of deleted function ‘std::unique_ptr<_Tp [], _Dp>::unique_ptr(_Up*) [with _Up = Electron; <template-
 parameter-2-2> = void; _Tp = Particle; _Dp = std::default_delete<Particle []>]’

发生了什么事?

【问题讨论】:

  • 该错误只是设计问题的症状。您的设计应该最好地区分数组的创建(请记住,数组本身不是多态的)和数组的多态使用。
  • 用这条评论做你想做的事,因为它可能完全偏离轨道和/或实施的工作量太大,但是......我个人在(物理)模拟/数值实验方面的经验是一切都是已知的编译时间。我觉得有点奇怪,因此您使用的虚函数会严重减慢程序的速度。到目前为止,我已经设法完全避免了这些运行时的事情。
  • 编译时 BOOMS 是您最好的朋友。 :-) 这是unique_ptr 的一个特性,它将运行时错误转化为编译时错误。
  • @JorenHeit 是的,事情在编译时是已知的,这是正确的,但是我也在努力使编码变得干净。例如,在这个直接的例子中,我正在使用离子和电子,许多东西可以很方便地变形,所以我不会重新复制和粘贴所有东西。如果这就是你的意思。
  • "优点是我能够以非常方便的方式使用数组electrons[number].function()"...that's UB

标签: c++ arrays c++11 polymorphism unique-ptr


【解决方案1】:

std::unique_ptr 防止在脚上开枪,因为std::default_delete&lt;T[]&gt; 调用delete[],具有标准中指定的行为

如果删除表达式以一元 :: 运算符开头,则 在全局范围内查找释放函数的名称。否则, 如果删除表达式用于释放一个类对象,其 静态类型有一个虚析构函数,释放函数是 在动态类型的虚拟定义点选择一个 析构函数(12.4)。 117 否则,如果删除表达式用于 取消分配 T 类或其数组 的对象,静态和 对象的动态类型应相同和释放 函数名在T范围内查找。

换句话说,代码如下:

Base* p = new Derived[50];
delete[] p;

是未定义的行为。

它似乎在某些实现上有效——在那里,delete[] 调用查找分配的数组的大小并在元素上调用析构函数——这要求元素具有众所周知的大小。由于派生对象的大小可能不同,因此指针算法出错,并使用错误的地址调用析构函数。

让我们回顾一下您的尝试:

std::unique_ptr<Particle[]> electrons(new Electron[count]);

std::unique_ptr 的构造函数中有一个代码可以检测到这些违规行为,请参阅cppreference

std::unique_ptr<Particle, std::default_delete<Particle[]>> electrons(new Electron[count]);

是未定义的行为,您实际上是告诉编译器delete[] 是释放您推送到electrons 的构造函数的资源的有效方法,如上所述,这是不正确的。

...but wait, there is more (priceless comment by @T.C.):

对于加法或减法,如果表达式 P 或 Q 的类型为“指向 cv T 的指针”,其中 T 和数组元素类型不相似 ([conv.qual]),则行为未定义。 [注意:特别是,当数组包含派生类类型的对象时,指向基类的指针不能用于指针算术。 ——尾注]

这意味着不仅删除数组是未定义的行为,索引也是如此!

Base* p = new Derived[50]();
p[10].a_function(); // undefined behaviour

这对你意味着什么?这意味着您不应该多态地使用数组

使用多态性的唯一安全方法是使用std::unique_ptr 指向派生对象,例如std::vector&lt;std::unique_ptr&lt;Particle&gt;&gt;(我们在那里没有数组的多态使用,但那里的数组具有多态对象)

既然您提到性能至关重要,那么动态分配每个 Particle 会很慢 - 在这种情况下您可以:

  • 使用对象池
  • 利用享元模式
  • 重构它以避免继承
  • 直接使用std::vector&lt;Electron&gt;std::unique_ptr&lt;Electron[]&gt;

【讨论】:

  • 但我一直都这样使用它,而且效果很好。我现在只是惊呆了......那么我应该如何创建这个数组呢?
  • @bluecore 这就是未定义行为的工作原理。无效代码似乎可以正确运行,但在较新的编译器上或在任何代码更改后会崩溃。我会尽快更新我的答案。
  • std::unique_ptr 这就是我现在正在考虑的。即使我必须在代码的其他区域实现模板,因为我也在使用 Ions。这就是我需要具有相同 Base 类型的原因。
  • 您的new[] 代码完全是疯了,根本不起作用。您对其工作的任何观察都纯属偶然,仅此而已。
  • 对于您的第一个报价,[expr.delete]/3 似乎更直接。
【解决方案2】:

您的设计问题在于对象是派生的和多态的,但不是对象数组。

例如,Electron 可能包含 Particle 所没有的其他数据。那么Electron 对象的大小将不再与Particle 对象的大小相同。所以访问数组元素所需的指针算法将不再起作用。

这个问题存在于指向数组的原始指针以及unique_ptrto 数组。只有对象本身是多态的。如果您想在不冒slicing 风险的情况下使用它们,则需要一个指向多态对象的指针数组。

如果您寻找解释为什么应该避免这种设计的其他论据,您可以查看 Scott Meyers 的书“更有效的 C++”中标题为“第 3 条:从不以多态方式处理数组”的部分。

替代方案:改变你的设计

例如,使用真实类型的vector 创建您的对象。并使用向量指向多态Particle 指针以多态使用这些对象:

vector<Electron>myelectrons(count);   // my real object store 
vector<Particle*>ve(count, nullptr);  // my adaptor for polymorphic access
transform(myelectrons.begin(), myelectrons.end(), ve.begin(), 
                [](Particle&e){return &e;} );  // use algorithm to populate easlily 
for (auto x: ve)  // make plain use of C++11 to forget about container type and size
   x->function(); 

这里是live demo

【讨论】:

    【解决方案3】:

    使用 std::unique_ptr 的 std::vector 或 std::array(如果你知道有多少)。像这样的:

    #include <vector>
    #include <memory>
    
    class A
    {
    public:
    
        A() = default;
        virtual ~A() = default;
    };
    
    class B : public A
    {
    public:
    
        B() = default;
        virtual ~B() = default;
    };
    
    int main(void)
    {
        auto v = std::vector<std::unique_ptr<A>>();
    
        v.push_back(std::make_unique<A>());
        v.push_back(std::make_unique<B>());
    
        return 0;
    }
    

    编辑:就速度而言,我用 3 种方法进行了快速测试,结果如下:

    Debug
    
    6.59999430  : std::vector (with reserve, unique_ptr)
    5.68793220  : std::array (unique_ptr)
    4.85969770  : raw array (new())
    
    Release
    
    4.81274890  : std::vector (with reserve, unique_ptr)
    4.42210580  : std::array (unique_ptr)
    4.12522340  : raw array (new())
    

    最后,我做了一个测试,我对所有 3 个版本都使用了 new() 而不是 unique_ptr:

    4.13924640 : std::vector
    4.14430030 : std::array
    4.14081580 : raw array
    

    因此,您会发现在发布版本中确实没有区别,其他一切都相同。

    【讨论】:

    • 我的应用程序中 std::vector 的问题是它很慢。原始数组的访问速度比动态创建的 std::vector 快 30%。请记住,粒子的数量可能以百万计,这使得 std::vector 无用。数组可能有效,但我在头文件中定义数组,例如 std::array var;不起作用,因为它需要在编译时知道大小。
    • @bluecore:std::vector 如何比使用new[] 创建的数组慢?
    • 您可以预先为 std::vector 保留一个合适的大尺寸以避免重复保留/复制。我的意思是你可能会使用动态创建的原始数组来做到这一点。
    • @GingerPlusPlus 尝试做 std::vector var;然后 double* var = new double[];数组大小为 10 000 000。现在将其放入 for 循环并进行一些算术运算并观察时间,您会感到惊讶。更不用说您实际上必须先将 push_back() 放入向量中。这真的一点也不奇怪。 std::vector 是动态分配的字段,容易产生内存碎片等,而 new[] 只是一个简单的连续内存块。
    • 仅在调试版本中。在发布版本中,它应该几乎一样快。
    【解决方案4】:

    令人惊讶的是,还没有人建议只使用多态删除器。 unique_ptr 的默认删除器只是一个默认值。你可以改变它来做任何你想做的事情,包括向上或向下投射。

    它涉及一些强制转换,但如果您愿意,可以将其隐藏在合适的界面后面。

    http://coliru.stacked-crooked.com/a/35bd4c3674d7df07

    不过,我不建议用它来做指针索引。那仍然会被彻底破坏。

    【讨论】:

    • 现在这真的很有趣,但对我来说也像是一个黑魔法。此外,它可能会使许多其他事情过于复杂。但是,感谢您的回答,将来可能会有用。
    【解决方案5】:

    如果您想保持接近当前代码并单独跟踪计数,您可以使用std::unique_ptr&lt;std::unique_ptr&lt;Particle&gt;[]&gt;

    请注意,这不会让您绕过额外的间接,如果可以的话,使用std::vector&lt;std::unique_ptr&lt;Particle&gt;&gt; 并因此包括reserve 的长度和明智的使用应该不会变慢。

    【讨论】:

    • 啊,就在我发布我的解决方案时,我阅读了您(完全相同)的回复。 :-)
    猜你喜欢
    • 1970-01-01
    • 2014-06-28
    • 2022-01-07
    • 1970-01-01
    • 1970-01-01
    • 2013-06-29
    • 1970-01-01
    • 2020-08-17
    • 1970-01-01
    相关资源
    最近更新 更多