Is virtual table creation thread safe?

From: https://isocpp.org/wiki/faq/strange-inheritance#calling-virtuals-from-ctors

You can call a virtual function in a constructor, but be careful. It may not do what you expect. In a constructor, the virtual call mechanism is disabled because overriding from derived classes hasn’t yet happened. Objects are constructed from the base up, “base before derived”.

If the "construction phase" has not finished by the time your async function gets call it will call the calling object's function.

Is setting the vtable by the compiler, thread safe?

To my understanding, it is not thread safe, but no one should be modifying that memory location except the allocator and initializer


First off a few excerpts from the standard that are relevant in this context:

[defns.dynamic.type]

type of the most derived object to which the glvalue refers [Example: If a pointer p whose static type is "pointer to class B" is pointing to an object of class D, derived from B, the dynamic type of the expression *p is "D". References are treated similarly. — end example]

[intro.object] 6.7.2.1

[..] An object has a type. Some objects are polymorphic; the implementation generates information associated with each such object that makes it possible to determine that object's type during program execution.

[class.cdtor] 11.10.4.4

Member functions, including virtual functions, can be called during construction or destruction. When a virtual function is called directly or indirectly from a constructor or from a destructor, including during the construction or destruction of the class's non-static data members, and the object to which the call applies is the object (call it x ) under construction or destruction, the function called is the final overrider in the constructor's or destructor's class and not one overriding it in a more-derived class. [..]

As you wrote, it is clearly defined how virtual function calls in the constructor/destructor work - they depend on the dynamic type of the object, and the dynamic type information associated with the object, and that information changes in the course of the execution. It is not relevant what kind of pointer you are using to "look at the object". Consider this example:

struct Base {
  Base() {
    print_type(this);
  }

  virtual ~Base() = default;

  static void print_type(Base* obj) {
      std::cout << "obj has type: " << typeid(*obj).name() << std::endl;
  }
};

struct Derived : public Base {
  Derived() {
    print_type(this);
  }
};

print_type always receives a pointer to Base, but when you create an instance of Derived you will see two lines - one with "Base" and one with "Derived". The dynamic type is set at the very beginning of the constructor so you can call a virtual function as part of the member initialization.

It is not specified how or where this information is stored, but it is associated with the object itself.

[..] the implementation generates information associated with each such object [..]

In order to change the dynamic type, this information has to be updated. This may be some data that is introduced by the compiler, but operations on that data are still covered by the memory model:

[intro.memory] 6.7.1.3

A memory location is either an object of scalar type or a maximal sequence of adjacent bit-fields all having nonzero width. [ Note: Various features of the language, such as references and virtual functions, might involve additional memory locations that are not accessible to programs but are managed by the implementation. — end note]

So the information associated with the object is stored and updated in some memory location. But that is were data races happen:

[intro.races]

[..]
Two expression evaluations conflict if one of them modifies a memory location and the other one reads or modifies the same memory location.
[..]
The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other [..]

The update of the dynamic type is not atomic, and since there is no other synchronization that would enforce a happens-before order, this is a data race and therefore UB.

Even if the update were to be atomic, you would still have no guarantee about the state of the object as long as the constructor has not finished, so there is no point of making it atomic.


Update

Conceptually it feels like the object takes on different types during construction and destruction. However, it has been pointed out to me by @LanguageLawyer that the dynamic type of an object (more precisely of a glvalue that refers to that object) corresponds to the most derived type, and this type is clearly defined and does not change. [class.cdtor] also includes a hint about this detail:

[..] the function called is the final overrider in the constructor's or destructor's class and not one overriding it in a more-derived class.

So even though the behavior of virtual function calls and the typeid operator is defined as if the object takes on different types, that is actually not the case.

That said, in order to achieve the specified behavior something in the state of the object (or at least some information associated with that object) has to be changed. And as pointed out in [intro.memory], these additional memory locations are indeed subject of the memory model. So I still stand by my initial assessment that this is a data race.