In my code for numerical physics, I need to create an array of Derived objects using the unique_ptr
with their type being the Base class. Normally, I would have:
// 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
};
Later in my code, to create an array of Derived objects with the Base type pointer, I would do
Particle* electrons = new Electron[count];
The advantage is that I am able to use the array in a really convenient way of electrons[number].function()
, because the incremental value in []
is actually the address of the memory that points to the proper instance of the object Electron
in the array. However, using raw pointers gets messy, so I decided to use the smart pointers.
Problem is with the definition of the Derived objects. I can do the following:
std::unique_ptr<Particle, std::default_delete<Particle[]>> electrons(new Electron[count]);
which creates the array of polymorphic Electrons and uses even the proper call of delete[]
. The problem lies in the way of calling the specific objects of the array, as I have to do this:
electrons.get()[number].function();
and I don't like the get()
part, not a little bit.
I could do the following:
std::unique_ptr<Particle[]> particles(new Particle[count]);
and yes, call the instances of Particle
type in the array with the
particles[number].function();
and everything would be fine and dandy, except for the part that I am not using the specific details of the class Electron
, therefore the code is useless.
And now for the funny part, let's do one more thing, shall we?
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 []>]’
What is going on?
std::unique_ptr
is preventing from shooting yourself in the foot, as std::default_delete<T[]>
calls delete[]
, which has the behaviour specified in the standard
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.
In other words, code like this:
Base* p = new Derived[50];
delete[] p;
is undefined behaviour.
It may have seem to work on some implementations - there, the delete[]
call looks up the size of the allocated array and calls destructors on the elements - which requires the elements to have a well known size. Since the size of derived objects may differ, the pointer arithmetic goes wrong, and the destructors are called with the wrong address.
Let's review what you tried:
std::unique_ptr<Particle[]> electrons(new Electron[count]);
there's a code in std::unique_ptr
's constructor that detects these violations, see cppreference.
std::unique_ptr<Particle, std::default_delete<Particle[]>> electrons(new Electron[count]);
is undefined behaviour, you essentially tell the compiler that delete[]
is a valid way to release the resources you push to the constructor of electrons
, which isn't true, as mentioned above.
...but wait, there is more (priceless comment by @T.C.):
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 ]
This means not only deleting an array is undefined behaviour, but so is indexing!
Base* p = new Derived[50]();
p[10].a_function(); // undefined behaviour
What does it mean to you? This means you shouldn't use arrays polymorphically.
The only safe way with polymorphism is to use std::unique_ptr
pointing to derived objects, like std::vector<std::unique_ptr<Particle>>
(we don't have polymorphic use of array there, but arrays with polymorphic objects there)
Since you mention that performance is critical, then dynamically allocating every Particle
will be slow - in this case you can:
std::vector<Electron>
or std::unique_ptr<Electron[]>
directly.The problem with your design is that objects are derived and polymorphic, but not the arrays of objects.
For example, Electron
could have additional data that a Particle
doesn't have. Then the size of an Electron
object would no longer be the same size as a Particle
object. So the pointer arithmetic that is needed to access array elements would not work anymore.
This problem exist for raw pointers to array as well as for unique_ptr
to array. Only the objects themselves are polymorphic. If you want to use them without the risk of slicing, you'd need an array of pointers to polymorphic objects.
If you look for additional arguments explaining why this design should be avoided, you may have a look at the section of Scott Meyers' book "More effective C++" titled "Item 3: never treat arrays polymorphically".
For example, use a vector
of the real type to create your objects. And use a vector to a polymorphic Particle
pointer to use these objects polymorphically:
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();
Here a live demo:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With