The problem I've encountered is easily reproducible with these two objects related by 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
which is stored in a std::stack
:
using Base = std::reference_wrapper<B>;
using Bases = std::stack<Base>;
Bases bases;
And two functions that make use of these aliases:
void push(B &&b)
{
bases.push_back(b);
}
void update()
{
bases.top().get().update();
}
When I run it, both with GCC and CLang it seems to work:
int main()
{
push(D{});
update();
return 0;
}
0x7ffd064ecd68 virtual void D::update()
But if I add a destructor to B
:
struct B
{
virtual void update() = 0;
virtual ~B() = default; // <--- ¡Nueva línea!
};
GCC fails:
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 at runtime in CLang?
- Which compiler is behaving correctly?
- Does the code incur undefined behavior?
- The move constructor of
std::reference_wrapper
is deleted, but I build aB
with a temporary, how is it possible?
Let's simplify the code a bit:
The code will not compile neither in GCC nor in CLANG and the reason is very simple...
reference_wrapper
it does not have a constructor that admits ar-value
and it is something that makes all the sense in the world since after callingD{}
the object it is destroyed and hereference_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 or not the destructor is declared.
Well, in your example what happens is that the
reference_wrapper
is encapsulated inside a stackstd::stack
. And it is here that we begin to move in swampy terrain. While we can't create areference_wrapper
from an r_value , we can get around this protection by making the object believereference_wrapper
that the object is an l-value :What's going on? Easy.
func
receives an r-value but treats it internally as an l-value , so on the line*
the constructor is being called:And this constructor is indeed a valid constructor. Well, it stops being so the moment the object it references is destroyed, but that's another story.
We can verify this point using
std::move
:The fact is that in your example you are deceiving the
reference_wrapper
and that is why you do not get errors at compile time:Well, we've already seen that the code in the question is unsafe 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.
Both are doing well. The only difference between the two is that they handle the virtual function table differently and this causes GCC to realize that you are trying to call a destructor that no longer exists (the virtual function table has been cleaned up).
Obviously yes. The final behavior will depend on how the compiler manages 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 as the destructor is being declared virtually. Here we are using polymorphism, so if the destructors are not virtual we may have problems freeing dynamic memory and resources.
By declaring the virtual destructor we are forcing all the destructors of the inheritance to be called... while without said destructor only the destructor of
B
. This, as can be seen, has an impact in the case of GCC since the final behavior changes.I tried to prepare an answer about this by showing changes to the resulting assembly but the answer was too complicated... so I have not added that information in the interest of a cleaner and more readable answer