std :: unique_ptr <t []>带有派生对象数组,使用已删除的函数

时间:2015-11-01 11:36:52

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

在我的数值物理代码中,我需要使用unique_ptr创建一个Derived对象数组,其类型为Base类。通常情况下,我会:

// 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()非常方便的方式使用数组,因为[]中的增量值实际上是指向正确实例的内存的地址数组中的对象Electron。但是,使用原始指针变得混乱,所以我决定使用智能指针。

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

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]);

BOOM!

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 []>]’

发生了什么事?

5 个答案:

答案 0 :(得分:10)

std::unique_ptr阻止自己在脚下射击,std::default_delete<T[]>调用delete[],其具有标准中指定的行为

  

如果delete-expression以unary :: operator开头,那么   在全局范围内查找释放函数的名称。除此以外,   如果delete-expression用于解除分配其对象的类对象   static类型有一个虚拟析构函数,deallocation函数是   在动态类型的虚拟定义点选择一个   析构函数(12.4)。 117 否则,如果使用delete-expression   deallocate 一个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<std::unique_ptr<Particle>>(我们没有多态使用数组,但那里有多态对象的数组)

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

  • 使用对象池
  • 使用flyweight模式
  • 重构它以避免继承
  • 直接使用std::vector<Electron>std::unique_ptr<Electron[]>

答案 1 :(得分:7)

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

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

对于数组的原始指针以及unique_ptr到数组的原始指针存在此问题。只有对象本身是多态的。如果你想在没有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

答案 2 :(得分:4)

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

#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

所以你看到发布版本没有什么区别,其他条件相同。

答案 3 :(得分:3)

令人惊讶的是,还没有人建议使用多态删除器。 unique_ptr的默认删除器只是默认值。您可以将其更改为执行任何操作,包括向上或向下投射。

它涉及一些演员,但如果你愿意,你可以隐藏在合适的界面后面。

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

但是,我不建议用这个做指针索引。那仍然会完全被打破。

答案 4 :(得分:2)

如果您想要接近当前代码并且无论如何都要单独跟踪计数,您可以使用std::unique_ptr<std::unique_ptr<Particle>[]>

请注意,这不会让你无法获得额外的间接,如果可以的话,使用std::vector<std::unique_ptr<Particle>>并因此包括reserve的长度和明智的使用不应该是慢。