std::unique_ptr<T[]>带派生对象数组,使用deleted函数
std::unique_ptr<T[]> with an array of derived objects, use of deleted function
在我的数值物理代码中,我需要使用 unique_ptr
创建一个 Derived 对象数组,它们的类型是 Base class。通常,我会:
// 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
的实例。但是,使用原始指针会变得混乱,所以我决定使用智能指针。
派生对象的定义有问题。我可以做到以下几点:
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();
一切都会好起来的,除了我没有使用 class 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 []>]’
这是怎么回事?
您设计的问题在于 objects 是派生的和多态的,而不是 objects 的数组。
例如,Electron
可能有 Particle
没有的额外数据。那么 Electron
object 的大小将不再与 Particle
object 的大小相同。因此访问数组元素所需的指针算法将不再有效。
指向数组的原始指针以及指向数组的 unique_ptr
都存在此问题。只有 object 本身是多态的。如果你想在没有 slicing 风险的情况下使用它们,你需要一个指向多态 objects.
的指针数组
如果您要寻找解释为什么应避免这种设计的其他论据,您可以查看 Scott Meyers 的书“更有效的 C++”中标题为“第 3 项:永远不要以多态方式处理数组”的部分。
备选方案:更改设计
例如,使用真实类型的 vector
来创建您的 object。并使用指向多态 Particle
指针的向量来多态地使用这些 object:
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:
std::unique_ptr
防止搬起石头砸自己的脚,因为 std::default_delete<T[]>
调用 delete[]
,它具有标准
中指定的行为
If a delete-expression begins with a unary :: operator, the
deallocation function’s name is looked up in global scope. Otherwise,
if the delete-expression is used to deallocate a class object whose
static type has a virtual destructor, the deallocation function is the
one selected at the point of definition of the dynamic type’s virtual
destructor (12.4). 117 Otherwise, if the delete-expression is used to
deallocate an object of class T or array thereof, the static and
dynamic types of the object shall be identical and the deallocation
function’s name is looked up in the scope of 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
的构造函数的资源的有效方式,如上所述,这是不正确的。
For addition or subtraction, if the expressions P or Q have type “pointer to cv T”, where T and the array element type are not similar ([conv.qual]), the behavior is undefined. [ Note: In particular, a pointer to a base class cannot be used for pointer arithmetic when the array contains objects of a derived class type. — end note ]
这意味着不仅删除数组是未定义的行为,索引也是!
Base* p = new Derived[50]();
p[10].a_function(); // undefined behaviour
这对你意味着什么?这意味着你不应该多态地使用数组。
使用多态性的唯一安全方法是使用 std::unique_ptr
指向派生对象,例如 std::vector<std::unique_ptr<Particle>>
(我们在那里没有对数组的多态使用,但是那里有具有多态对象的数组)
既然你提到性能很重要,那么动态分配每个 Particle
会很慢 - 在这种情况下你可以:
- 使用对象池
- 使用享元模式
- 重构它以避免继承
- 直接使用
std::vector<Electron>
或std::unique_ptr<Electron[]>
。
使用 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::unique_ptr<std::unique_ptr<Particle>[]>
。
请注意,这不会让您绕过额外的间接寻址,如果可以的话,使用 std::vector<std::unique_ptr<Particle>>
并因此包括长度和 reserve
的明智使用不应该是较慢。
令人惊讶的是,还没有人建议只使用多态删除器。 unique_ptr 的默认删除器就是一个默认值。你可以改变它做任何你想做的事,包括上投或下投。
它涉及一些转换,但如果需要,您可以将其隐藏在合适的界面后面。
http://coliru.stacked-crooked.com/a/35bd4c3674d7df07
不过,我不建议用它来做指针索引。那还是彻底坏掉了。
在我的数值物理代码中,我需要使用 unique_ptr
创建一个 Derived 对象数组,它们的类型是 Base class。通常,我会:
// 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
的实例。但是,使用原始指针会变得混乱,所以我决定使用智能指针。
派生对象的定义有问题。我可以做到以下几点:
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();
一切都会好起来的,除了我没有使用 class 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 []>]’
这是怎么回事?
您设计的问题在于 objects 是派生的和多态的,而不是 objects 的数组。
例如,Electron
可能有 Particle
没有的额外数据。那么 Electron
object 的大小将不再与 Particle
object 的大小相同。因此访问数组元素所需的指针算法将不再有效。
指向数组的原始指针以及指向数组的 unique_ptr
都存在此问题。只有 object 本身是多态的。如果你想在没有 slicing 风险的情况下使用它们,你需要一个指向多态 objects.
如果您要寻找解释为什么应避免这种设计的其他论据,您可以查看 Scott Meyers 的书“更有效的 C++”中标题为“第 3 项:永远不要以多态方式处理数组”的部分。
备选方案:更改设计
例如,使用真实类型的 vector
来创建您的 object。并使用指向多态 Particle
指针的向量来多态地使用这些 object:
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:
std::unique_ptr
防止搬起石头砸自己的脚,因为 std::default_delete<T[]>
调用 delete[]
,它具有标准
If a delete-expression begins with a unary :: operator, the deallocation function’s name is looked up in global scope. Otherwise, if the delete-expression is used to deallocate a class object whose static type has a virtual destructor, the deallocation function is the one selected at the point of definition of the dynamic type’s virtual destructor (12.4). 117 Otherwise, if the delete-expression is used to deallocate an object of class T or array thereof, the static and dynamic types of the object shall be identical and the deallocation function’s name is looked up in the scope of 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
的构造函数的资源的有效方式,如上所述,这是不正确的。
For addition or subtraction, if the expressions P or Q have type “pointer to cv T”, where T and the array element type are not similar ([conv.qual]), the behavior is undefined. [ Note: In particular, a pointer to a base class cannot be used for pointer arithmetic when the array contains objects of a derived class type. — end note ]
这意味着不仅删除数组是未定义的行为,索引也是!
Base* p = new Derived[50]();
p[10].a_function(); // undefined behaviour
这对你意味着什么?这意味着你不应该多态地使用数组。
使用多态性的唯一安全方法是使用 std::unique_ptr
指向派生对象,例如 std::vector<std::unique_ptr<Particle>>
(我们在那里没有对数组的多态使用,但是那里有具有多态对象的数组)
既然你提到性能很重要,那么动态分配每个 Particle
会很慢 - 在这种情况下你可以:
- 使用对象池
- 使用享元模式
- 重构它以避免继承
- 直接使用
std::vector<Electron>
或std::unique_ptr<Electron[]>
。
使用 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::unique_ptr<std::unique_ptr<Particle>[]>
。
请注意,这不会让您绕过额外的间接寻址,如果可以的话,使用 std::vector<std::unique_ptr<Particle>>
并因此包括长度和 reserve
的明智使用不应该是较慢。
令人惊讶的是,还没有人建议只使用多态删除器。 unique_ptr 的默认删除器就是一个默认值。你可以改变它做任何你想做的事,包括上投或下投。
它涉及一些转换,但如果需要,您可以将其隐藏在合适的界面后面。
http://coliru.stacked-crooked.com/a/35bd4c3674d7df07
不过,我不建议用它来做指针索引。那还是彻底坏掉了。