C++ 标准::唯一性\u ptr<;T[]>;对于派生对象数组,使用deleted函数

C++ 标准::唯一性\u ptr<;T[]>;对于派生对象数组,使用deleted函数,c++,arrays,c++11,polymorphism,unique-ptr,C++,Arrays,C++11,Polymorphism,Unique Ptr,在我的数值物理代码中,我需要使用unique\u ptr创建一个派生对象数组,其类型为基类。通常,我会: // Header file of the Base class class Particle{ public: Particle(); // some constructor virtual ~Particle(); // virtual destructor because of polymorphism virtual functi

在我的数值物理代码中,我需要使用
unique\u 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
};

优点是我能够以非常方便的方式使用数组,即
electronics[number].function()
,因为
[]
中的增量值实际上是指向数组中对象
Electron
的适当实例的内存地址。然而,使用原始指针会变得混乱,所以我决定使用智能指针

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

std::unique_ptr<Particle, std::default_delete<Particle[]>> electrons(new Electron[count]);
std::unique_ptr<Particle[]> particles(new Particle[count]);
我一点也不喜欢
get()
部分

我可以做到以下几点:

std::unique_ptr<Particle, std::default_delete<Particle[]>> electrons(new Electron[count]);
std::unique_ptr<Particle[]> particles(new Particle[count]);
除了我没有使用类
Electron
的特定细节的部分之外,所有的事情都会很好,因此代码是无用的

有趣的是,让我们再做一件事,好吗

std::unique_ptr<Particle[]> electrons(new Electron[count]);
std::唯一电子(新电子[计数]);

使用删除的函数“std::unique\u ptr::unique\u ptr(\u Up*)[带_Up=Electron;=void;\u Tp=Particle;\u Dp=std::default\u delete]”

发生了什么?

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

例如,
电子
可能具有
粒子
不具有的附加数据。然后,
电子
对象的大小将不再与
粒子
对象的大小相同。因此,访问数组元素所需的指针算法将不再有效

对于指向数组的原始指针以及指向数组的
唯一\u ptr
指针,都存在此问题。只有对象本身是多态的。如果您想使用它们而不冒风险,您需要一个指向多态对象的指针数组

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

备选方案:更改您的设计 例如,使用实类型的
向量
创建对象。并使用指向多态
粒子的向量
指针以多态方式使用这些对象:

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(); 
vectormyelectrons(计数);//我的实物商店
向量(count,nullptr);//我的多态访问适配器
转换(myelectrons.begin(),myelectrons.end(),ve.begin(),
[](粒子&e){return&e;});//使用算法轻松填充
for(auto x:ve)//简单地使用C++11来忘记容器类型和大小
x->function();

这里有一个:

std::unique\u ptr
防止射中你自己的脚,因为
std::default\u delete
调用了
delete[]
,这具有标准中规定的行为

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

换句话说,代码如下所示:

Base* p = new Derived[50];
delete[] p;
#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;
}
这是一种未定义的行为

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

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

std::unique_ptr<Particle[]> electrons(new Electron[count]);
如果是未定义的行为,您基本上会告诉编译器,
delete[]
是释放推送到
electrons
构造函数的资源的有效方法,如上所述,这是不正确的

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

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

Base* p = new Derived[50]();
p[10].a_function(); // undefined behaviour
这对你意味着什么?这意味着您不应该以多态方式使用数组

多态性唯一安全的方法是使用
std::unique_ptr
指向派生对象,如
std::vector
(我们没有多态性地使用数组,但是数组中有多态性对象)

由于您提到性能至关重要,因此动态分配每个
粒子将非常缓慢-在这种情况下,您可以:

  • 使用对象池
  • 利用飞锤模式
  • 重构它以避免继承
  • 直接使用
    std::vector
    std::unique\u ptr

使用std::vector或std::array(如果您知道有多少个)std::unique\u ptr。大概是这样的:

Base* p = new Derived[50];
delete[] p;
#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个版本使用new(),而不是unique_ptr:

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

因此,您可以看到,在其他条件相同的情况下,发布版本中实际上没有任何差异。

如果您希望保持与当前代码相近的状态,并单独跟踪计数,则可以使用
std::unique\u ptr

注意
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())
4.13924640 : std::vector
4.14430030 : std::array
4.14081580 : raw array