Sunday, December 13, 2020

Digest for comp.lang.c++@googlegroups.com - 17 updates in 3 topics

Bonita Montero <Bonita.Montero@gmail.com>: Dec 13 10:39AM +0100

I had a nice bug: I've got a base-class with a pure virtual function
which is overridden in a derived class. This function is virtually
called by a thread from a thread pool (through a static proxy-func-
tion). In the destructor of the base-class I wait for all threads
from the thread pool to have finished their work and that there are
no more items in their work-queue.
What I missed was, that as soon as the base-class destructor is called
the virtual fuction pointer is adjusted to the pointer of the base-class
(I'm wondering that is there any because a base-class with a pure vir-
tual function shouldn't have a virtual method table at all). So before
I could wait for the threads to finish all work the null pure virtual
function got called. ;-)
Interestingly this doesn't happen if I don't compile the code as debug
-code. So the compiler indirectly gives me a hint here for a possible
bug.
Richard Damon <Richard@Damon-Family.org>: Dec 13 08:05AM -0500

On 12/13/20 4:39 AM, Bonita Montero wrote:
> Interestingly this doesn't happen if I don't compile the code as debug
> -code. So the compiler indirectly gives me a hint here for a possible
> bug.
 
The vtable change is because once you enter the base class destructor,
there isn't any derived class to enter anymore (it has been destructed),
so the object is now just of the base class.
 
This says either you have an ownership issue with the object, where you
are destroying it before all the users are done with it, or the wait in
the destructor is surperfilous, as all the threads should be done before
you got there.
 
Maybe you need that wait in each of the destructors of the derived
classes (maybe calling a method in the base), but even that is
technically late as you shouldn't be doing even that if there are still
things referring to it.
Marcel Mueller <news.5.maazl@spamgourmet.org>: Dec 13 02:35PM +0100

Am 13.12.20 um 14:05 schrieb Richard Damon:
 
> The vtable change is because once you enter the base class destructor,
> there isn't any derived class to enter anymore (it has been destructed),
> so the object is now just of the base class.
 
Yes that is the way C++ initializes and destroys the objects.
 
While formally correct I never found a practical use case for this behavior.
In contrast it creates the need for initialization or deinitialization
functions that need to be called manually for every class instance. So
it is correct but useless. From my point of view a language must be
first of all useful.
 
 
Marcel
Bonita Montero <Bonita.Montero@gmail.com>: Dec 13 02:39PM +0100

> The vtable change is because once you enter the base class destructor,
> there isn't any derived class to enter anymore (it has been destructed),
> so the object is now just of the base class.
 
As you can read from my posting I already know that.
 
> classes (maybe calling a method in the base), but even that is
> technically late as you shouldn't be doing even that if there are
> still things referring to it.
 
I'm using a simple function-pointer now.
Paavo Helde <myfirstname@osa.pri.ee>: Dec 13 04:00PM +0200

13.12.2020 11:39 Bonita Montero kirjutas:
> tual function shouldn't have a virtual method table at all). So before
> I could wait for the threads to finish all work the null pure virtual
> function got called. ;-)
 
That's what std::shared_ptr is for. By its design you cannot have such
problems (either during construction or destruction) if you access your
shared object via std::shared_ptr pointers only. Whenever you have a
shared_ptr to an object, you can be sure that it points to the most
derived class object (because a shared_ptr cannot be constructed before
the most derived class object is ready, and the destructor of the most
derived class is not evoked while there is a shared_ptr alive).
 
Just stop trying to be too clever for no good, always access your shared
objects via shared_ptr, and adjust the rest of the code to cope with
that simple rule. That's it, a major source of nasty heisenbugs is gone!
Juha Nieminen <nospam@thanks.invalid>: Dec 13 02:07PM

> derived class object (because a shared_ptr cannot be constructed before
> the most derived class object is ready, and the destructor of the most
> derived class is not evoked while there is a shared_ptr alive).
 
I don't understand what std::shared_ptr has anything to do with this.
 
The problem was that the destructor of the base class was calling a pure
virtual method (something that a good compiler should warn about, but
it's not an enforceable error).
 
I don't think std::shared_ptr would have remedied that in any way.
When the last std::shared_ptr object is destroyed, it will destroy the
managed object, and the problem will happen. std::shared_ptr doesn't
somehow magically guard against that problem.
Bonita Montero <Bonita.Montero@gmail.com>: Dec 13 03:13PM +0100

> The problem was that the destructor of the base class was calling a pure
> virtual method (something that a good compiler should warn about, but
> it's not an enforceable error).
 
Not the destructor but another function from another thread to the same
object.
Bonita Montero <Bonita.Montero@gmail.com>: Dec 13 03:14PM +0100

> derived class object (because a shared_ptr cannot be constructed before
> the most derived class object is ready, and the destructor of the most
> derived class is not evoked while there is a shared_ptr alive).
 
shared_ptr<> has nothing to do with my issue.
Paavo Helde <myfirstname@osa.pri.ee>: Dec 13 04:27PM +0200

13.12.2020 16:07 Juha Nieminen kirjutas:
 
> The problem was that the destructor of the base class was calling a pure
> virtual method (something that a good compiler should warn about, but
> it's not an enforceable error).
 
If you call a pure virtual method from dtor, you get guaranteed crash
each time, not a nasty timings-dependent multithreading bug which Bonita
was describing.
 
> When the last std::shared_ptr object is destroyed, it will destroy the
> managed object, and the problem will happen. std::shared_ptr doesn't
> somehow magically guard against that problem.
 
Yes it does. When you call a member function of an object via
std::shared_ptr then it means its destructor has not yet started
(neither in this nor in another thread), so there is no danger to call a
pure virtual method.
Bonita Montero <Bonita.Montero@gmail.com>: Dec 13 06:55PM +0100

Here's the current state of my thread-pool-class:
 
// debug_exceptions.h
 
#pragma once
#if !defined try_debug
#if !defined(NDEBUG)
#define try_debug try
#else
#define try_debug

No comments: