If ipc calls the lambda on a separate thread then is there a data race in this code? If the vtable is not fully constructed yet then the lambda may call Base::Handle() but if it is then it will call Derived::Handle(), right?
#include <thread>
#include <chrono>
#include <functional>
#include <iostream>
#include <mutex>
#include <condition_variable>
struct IPC {
IPC() : t([&]{
std::unique_lock lk(mut);
cv.wait(lk, [&]{ return (bool)f; });
f();
}) {}
void SetHandler(std::function<void()> f_) {
{
std::unique_lock lk(mut);
f = f_;
}
cv.notify_all();
}
std::mutex mut;
std::condition_variable cv;
std::function<void()> f;
std::thread t;
};
struct Base {
Base() {
ipc.SetHandler([this]{ Handle(); });
// Maybe construction of Base takes some time.
// Removing this sleep makes the output of the program stabilize on "Derived".
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
virtual void Handle() { std::cout << "Base" << std::endl; }
IPC ipc;
};
struct Derived : Base {
void Handle() override { std::cout << "Derived" << std::endl; }
};
int main()
{
Derived b;
// I only care about interaction with constructors so join here
// to delay destruction of b until the test is finished.
b.ipc.t.join();
}
Running this code multiple times in succession on my laptop produces randomly alternating outputs of "Base" and "Derived".
developer@WDX591VWD3:~$ g++-10 -std=c++20 hi.cpp -lpthread
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Derived
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Derived
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Derived
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Base
developer@WDX591VWD3:~$ ./a.out
Derived
developer@WDX591VWD3:~$
So I guess the question is: Is this undefined behavior? How long do I need to wait before IPC would be able to call the handler such that the derived class handler is called? Do I need to do any synchronization?
That's going to have undefined behavior.
While I think that the standard currently lacks some wording to that effect, it is in my opinion intended that the object may be used in another thread only after the construction of the most-derived object has finished.
A similar provision is given in [class.cdtor]/2 when trying to access the object through a pointer that is not derived from
this.I also expect that it isn't intended to matter whether or not the class is polymorphic. The restriction linked above also doesn't apply only to polymorphic classes.