Is virtual table creation thread safe?

Viewed 521

Please let me begin with that I know it is a bad practice to call virtual functions from within a constructor/destructor. However, the behavior in doing so, although it might be confusing or not what the user is expecting, is still well defined.

struct Base
{
    Base()
    {
        Foo();
    }
    virtual ~Base() = default;
    virtual void Foo() const
    {
        std::cout << "Base" << std::endl;
    }
};

struct Derived : public Base
{
    virtual void Foo() const
    {
        std::cout << "Derived" << std::endl;
    }
};

int main(int argc, char** argv) 
{
    Base base;
    Derived derived;
    return 0;
}

Output:
Base
Base

Now, back to my real question. What happens if a user calls a virtual function from within the constructor from a different thread. Is there a race condition? Is it undefined? Or put it in other words. Is setting the vtable by the compiler, thread-safe?

Example:

struct Base
{
    Base() :
        future_(std::async(std::launch::async, [this] { Foo(); }))
    {
    }
    virtual ~Base() = default;

    virtual void Foo() const
    {
        std::cout << "Base" << std::endl;
    }

    std::future<void> future_;
};

struct Derived : public Base
{
    virtual void Foo() const
    {
        std::cout << "Derived" << std::endl;
    }
};

int main(int argc, char** argv) 
{
    Base base;
    Derived derived;
    return 0;
}

Output:
?
4 Answers

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.

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

I believe [class.base.init]/16:

Member functions (including virtual member functions) can be called for an object under construction. Similarly, an object under construction can be the operand of the typeid operator or of a dynamic_­cast. However, if these operations are performed in a ctor-initializer (or in a function called directly or indirectly from a ctor-initializer) before all the mem-initializers for base classes have completed, the program has undefined behavior.

should answer the question. However, it is defective. The fix would be

However, if these operations are performed in a ctor-initializer (or in a function called directly or indirectly from a ctor-initializer) before not after all the mem-initializers for base classes have completed, the program has undefined behavior.

Currently, the paragraph says that the behavior is undefined only if the invocation of a member function happens before mem-initializers for base classes have completed, but doesn't cover your case: when the invocation neither happens before base classes initialization completion nor base classes initialization completion happens before the invocation.

Yes.

Strictly speaking, no. You probably can, with some effort, construct a malicious example which is, at least formally, not thread-safe. It will still be thread-safe in practice, though.

Other than trying to be outright maliciously obnoxious, and in particular in the context of your question, however, it's definitively yes.

An object is either not present at all or being constructed, or fully constructed. The only state that is somehow awkward is the being-constructed one.
It is sometimes discouraged to call virtual functions from constructors on partially-constructed objects, but it is entirely legitimate to do so, as long as one is aware of the implications. That is, the same object is different things at different times.

As for your concrete example: What you are doing is, you call a function, for which you capture this, which at that time refers to an object of type Base. There is not much doubt about what type of object the thread will use when it eventually gets to run because it must use that one copy of this which is just that, and nothing else.

The standard does not define exactly how things work with inheritance and vtables and all that (but that's merely unspecified, not Undefined Behavior). In practice, it's usually a pointer to a static structure that is being updated, and on most architectures (all reasonable architectures, for that matter) that's an atomic operation anyway.
So even if there needed to be some sort of atomicity, it would in practice be there anyway.

It isn't even needed, though. The capture happens from exactly one thread, which is the thread that is currently constructing the object, and there is only a single state the object could possibly be in, and there is no doubt about what it is.

Related