c++ – Adding a virtual destructor to a virtual class causes a runtime crash (GCC vs CLang)

Question:

The problem I have found is easily reproducible with these two related objects via inheritance:

struct B
{
    virtual void update() = 0;
};

struct D : B
{
    void update() override
    {
        std::cout << this << ' ' << __PRETTY_FUNCTION__ << '\n';
    }
};

I have some aliases that define a wrapper on B that is stored in a std::stack :

using Base = std::reference_wrapper<B>;
using Bases = std::stack<Base>;

Bases bases;

And two functions that use these aliases:

void push(B &&b)
{
    bases.push_back(b);
}

void update()
{
    bases.top().get().update();
}

When I run it with both GCC and CLang it seems to work:

int main()
{
    push(D{});
    update();

    return 0;
}
0x7ffd064ecd68 virtual void D::update()

But if I add a destroyer to B :

struct B
{
    virtual void update() = 0;
    virtual ~B() = default; // <--- ¡Nueva línea!
};

GCC falla:

pure virtual method called
terminate called without an active exception

Aborted

But CLang seems to work:

0x7ffdffc31500 virtual void D::update()

  • Why does adding a virtual destructor fail at runtime in GCC but not fail at runtime in CLang?
  • Which compiler is behaving correctly?
  • Does the code incur undefined behavior?
  • The std::reference_wrapper move constructor is deleted, but I build a B with a temporary how is it possible ?.

Answer:

The std :: reference_wrapper move constructor is deleted, but I build a B with a temporary how is it possible ?.

Let's simplify the code a bit:

int main()
{
  std::reference_wrapper<B> ref{D{}};
  return 0;
}

The code will not compile in GCC or CLANG and the reason is very simple … reference_wrapper does not have a constructor that supports an r-value and it makes all the sense in the world since after calling D{} the object is destroyed and the reference_wrapper will point to an item that is no longer valid … it's an obvious protection.

Needless to say, the above code will fail whether the destructor has been declared or not.

Well, in your example what happens is that the reference_wrapper is encapsulated within a std::stack . And this is where we begin to move on swampy terrain. While we cannot create a reference_wrapper from an r_value , we can bypass this protection by making the reference_wrapper believe that the object is an l-value :

void func(B && b)
{
  std::reference_wrapper<B> ref(b); // *
}

int main()
{
  std::reference_wrapper<B>(D{}); // ERROR
  func(D{});                      // OK!!!
}

What's going on? Easy. func receives an r-value but treats it internally as an l-value , then on line * the constructor is being invoked:

std::reference_wrapper<B>(B&)

And this constructor is indeed a valid constructor. Well, it ceases to be the moment the object it refers to is destroyed, but that's another story.

We can verify this point using std::move :

void func(B && b)
{
  std::reference_wrapper<B> ref(std::move(b)); // ERROR
}

The point is that in your example you are cheating the reference_wrapper and that is why you do not get errors at compile time:

void push(B &&b)
{
    bases.push(b); // <<--- b es un l-value
}

Why does adding a virtual destructor fail at runtime in GCC but not fail at runtime in CLang?

Well, we have already seen that the code in the question is insecure by definition. In this case, the expected behavior is indeterminate and depends solely and exclusively on the way in which each compiler manages the table of virtual functions.

Which compiler is behaving correctly?

They are both doing well. The only difference between the two is that they handle the virtual function table differently and this makes GCC realize that you are trying to invoke a destructor that no longer exists (the virtual function table has been cleaned up).

Does the code incur undefined behavior?

Obviously yes. The final behavior will depend on how the compiler handles the table of virtual functions.

Clarification

But … why does the behavior change when declaring the destructor?

The reason is not so much to declare the destructor but rather that the destructor is being declared virtually. Here we are using polymorphism, so if the destructors are not virtual we can have problems releasing dynamic memory and resources.

By declaring the virtual destructor we are forcing all the destructors of the inheritance to be invoked … while without said destructor only the destructor of B be invoked. This, as seen, has an impact in the case of GCC as the final behavior changes.

I have tried to prepare an answer on this showing changes to the resulting assembly but the answer was getting too complicated … so I have not added that information for the benefit of a cleaner and more readable answer

Scroll to Top