Guillaume Racicot Guillaume Racicot - 2 months ago 15
C++ Question

Can upcasting a shared_ptr<T> to a shared_ptr<void> lead to undefined behaviour?

Shared pointers are quite smart. They remember the type they where first constructed with in order to delete them correctly. Take that for example:

struct A { virtual void test() = 0; };
struct B : A { void test() override {} };

void someFunc() {
std::shared_ptr<A> ptr1;

ptr1 = std::make_shared<B>();

// Here at the end of the scope, B is deleted correctly
}


However, there seems to be a problem with void pointers: for a downcast of a void pointer to be valid, one must downcast it to the type it was originally upcasted from.

For example:

void* myB = new B;

// Okay, well defined
doStuff(static_cast<B*>(myB));

// uh oh, not good!
// For the same instance of a child object, a pointer to the base and
// a pointer to the child can be differrent.
doStuff(static_cast<A*>(myB));


With
std::shared_ptr
, when you use
std::make_shared
, the deleter must look similar to this function:
[](B* ptr){ delete ptr; }
. Since the pointer (in the first example) is holding a
B
instance in a pointer to
A
and deletes it correctly, it must downcast it in some way.

My question is: is the following code snippet invokes undefined behaviour?

void someFunc() {
{
std::shared_ptr<void> ptr = std::make_shared<B>();

// Deleting the pointer seems okay to me,
// the shared_ptr knows that a B was originally allocated with a B and
// will send the void pointer to the deleter that's delete a B.
}

std::shared_ptr<void> vptr;

{
std::shared_ptr<A> ptr = std::make_shared<B>();

// ptr is pointing to the base, which can be
// different address than the pointer to the child.

// assigning the pointer to the base to the void pointer.
// according to my current knowledge of void pointers,
// any future use of the pointer must cast it to a A* or end up in UB.
vptr = ptr;
}

// is the pointer deleted correctly or it tries to
// cast the void pointer to a B pointer without downcasting to a A* first?
// Or is it well defined and std::shared_ptr uses some dark magic unknown to me?
}

Answer

The code is correct.

std::shared_ptr internally saves the real pointer and the real deleter as they are in the constructor, so no matter how you downcast it, as long as the downcast is valid the deleter will be right.

The shared_ptr actually does not hold a pointer to the object, but a pointer to an intermediate struct that holds the actual object, the reference counter and the deleter. It doesn't matter if you cast the shared_ptr, that intermediate struct does not change. It cannot change because your vptr and ptr, although of different types, share the reference counter (and the object and the deleter, of course).

BTW, that intermediate struct is the reason for the make_shared optimization: it allocates both the intermediate struct and the object itself in the same memory block and avoids the extra allocation.

To illustrate how smart pointers can be, I have written a program with plain pointers that crashes (with GCC 6.2.1) because of your problem:

#include <memory>
#include <iostream>

struct A
{
    int a;
    A() :a(1) {}
    ~A()
    {
        std::cout << "~A " << a << std::endl;
    }
};

struct X
{
    int x;
    X() :x(3) {}
    ~X()
    {
        std::cout << "~X " << x << std::endl;
    }
};

struct B : X, A
{
    int b;
    B() : b(2) {}
    ~B()
    {
        std::cout << "~B " << b << std::endl;
    }
};

int main()
{
    A* a = new B;
    void * v = a;
    delete (B*)v; //crash!

    return 0;    
}

Actually it prints the wrong integer values, which proves the UB.

~B 0
~A 2
~X 1
*** Error in `./a.out': free(): invalid pointer: 0x0000000001629c24 ***

But the version with smart pointers works just fine:

int main()
{
    std::shared_ptr<void> vptr;

    {
        std::shared_ptr<A> ptr = std::make_shared<B>();
        vptr = ptr;
    }
    return 0;
}

It prints as expected:

~B 2
~A 1
~X 3
Comments