Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::unique_ptr<T[]> API prohibits derived-to-base pointer conversions

In Modern Effective C++, "Iterm 19: Use std::shared_ptr for shared-ownership resource management.", Page 133-134, it says:

std::shared_ptr supports derived-to-base pointer conversions that make sense for single objects, but that open holes in the type system when applied to arrays. (For this reason, the std::unique_ptr API prohibits such conversions.)

What's the meaning of "open holes in the type system"?

Why would std::unique_ptr<T[]> API prohibit derived-to-base pointer conversions?

And how could it prohibit the conversions?

like image 465
chaosink Avatar asked Aug 28 '18 05:08

chaosink


1 Answers

A hole in the type system is whenever the compiler doesn't catch when a type is cast to another incompatible type.

Imagine you have two simple classes:

class A
{
    char i;
};

class B : public A
{
    char j;
};

Let's for simplicity ignore things like padding etc. and assume that objects of type A are 1 byte and objects of type B are 2 byte.

Now when you have an array of type A or an array of type B, they will look like this:

A a[4]:

=================
| 0 | 1 | 2 | 3 |
|-------|-------|
| i | i | i | i |
=================

B b[4]:

=================================
|   0   |   1   |   2   |   3   |
|-------|-------|-------|-------|
| i | j | i | j | i | j | i | j |
=================================

Now imagine you have pointers to these arrays and then cast one to the other, this would obviously lead to problems:

a cast to B[4]:

=================================
|   0   |   1   |   2   |   3   |
|-------|-------|-------|-------|
| i | j | i | j | x | x | x | x |
=================================

The first two objects in the array will interpret the i member of the 2nd and 4th A as their j member. The 2nd and 3rd member access unallocated memory.

b cast to A[4]:

=================
| 0 | 1 | 2 | 3 |
|-------|-------|
| i | i | i | i | x | x | x | x |
=================

Here it's the other way around, all 4 objects now alternatingly interpret the i and the j of 2 B instances as their i members. And half of the array is lost.

Now imagine deleting such a casted array. Which destructors will be called? What memory will be freed? You are in deep hell at this point.

But wait, there's more.

Imagine you have 3 classes like this:

class A
{
    char i;
};

class B1 : public A
{
    float j;
};

class B2 : public A
{
    int k;
};

And now you create an array of B1 pointers:

B1* b1[4];

If you cast that array to an array of A pointers you could think, "well this is fine, right"?

A** a = <evil_cast_shenanigans>(b1);

I mean, you can safely access each member as pointer to A:

char foo = a[0]->i; // This is valid

But what you can also do, is this:

a[0] = new B2{};   // Uh, oh.

This is a valid assignment, no compiler will complain, but you must not forget that we are actually working on an array that was created as an array of pointers to B1 objects. And it's first member now points at a B2 object, which you can now access as B1 without the compiler saying a thing.

float bar = b1[0]->j;   // Ouch.

So again you are in deep hell, and the compiler won't be able to warn you, except if that upcasting isn't allowed in the first place.

Why would std::unique_ptr API prohibit derived-to-base pointer conversions?

I hope the above explanations give good reasons why.

How could it prohibit the conversions?

It simply doesn't provide any API to do conversions. The shared_ptr API has conversion functions like static_pointer_cast, the unique_ptr API doesn't.

like image 65
Max Vollmer Avatar answered Nov 01 '22 15:11

Max Vollmer