6

This is a scenario you shouldn't ever do, but https://timsong-cpp.github.io/cppwp/class.cdtor#4 states:

Member functions, including virtual functions ([class.virtual]), can be called during construction or destruction ([class.base.init]).

Does this hold if the functions are called in parallel? That is, ignoring the race condition, if the A is in the middle of construction, and frobme is called at some point AFTER the constructor is invoked (e.g. during construction), is that still defined behavior?

#include <thread>

struct A {
    void frobme() {}
};

int main() {
    char mem[sizeof(A)];

    auto t1 = std::thread([mem]() mutable { new(mem) A; });
    auto t2 = std::thread([mem]() mutable { reinterpret_cast<A*>(mem)->frobme(); });

    t1.join();
    t2.join();
}

As a separate scenario, it was also pointed out to me that it's possible for A's constructor to create multiple threads, where those those threads may invoke a member function function before A is finished construction, but the ordering of those operations would be more analyzable (you know no races will occur until AFTER the thread is generated in the constructor).

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Mike Lui
  • 1,301
  • 10
  • 23

2 Answers2

6

There are two issues here: your specific code and your general question.

In your specific code, even in the best possible case scenario (where t2 executes after t1), you have a data race due to the lack of synchronization between creation and use. And that makes your code UB regardless of the order of execution.

In the general question, let's assume that the constructor of a type hands the this pointer off to some other thread, which then calls functions on it, and the hand-off itself is properly synchronized. Would some other thread invoking a member function be considered a data race?

Well, it certainly would be a data race if the other thread invokes a function that reads member values or other data written by the constructor subsequent to the point of the hand-off, or if the constructor accesses members or other data written by the member function being invoked. That is, if there are no data races between the code being executed simultaneously.

Assuming that neither of those is the case, then everything should be fine (mostly. It's possible to define A in such a way that your reinterpret_cast doesn't return a usable pointer to the A you created in that storage; you'd need to launder it). An object under construction/destruction can be accessed, but only in certain ways. Stick to those ways, and you should be fine... with one possible catch.

There's nothing in the standard about data races on the completion of an object's initialization, only on conflicts in memory locations. Once the object is fully constructed, the behavior of virtual functions could change, based on changing vtable pointers and such if the dynamic type is a class derived from the class given to the other thread. I don't believe there's a clear statement about this in the section on the object model.

Also, note that C++20 added a special rule to class.cdtor:

During the construction of an object, if the value of the object or any of its subobjects is accessed through a glvalue that is not obtained, directly or indirectly, from the constructor's this pointer, the value of the object or subobject thus obtained is unspecified.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • I think @Ben Voight's answer regarding glvalues is key in this case. It's UB to use any glvalue of the object before lifetime has started, for non-static member functions. Using `this` in the constructor/initializer list is okay because it is a prvalue. – Mike Lui Mar 16 '20 at 21:13
  • @MikeLui: `this` is a prvalue, yes, but it's a value which is a *pointer*. Dereferencing a pointer creates... a glvalue. And it's through that glvalue that you access the object. This is *why* the section he referened had a *specific exception* for objects under construction/destruction. – Nicol Bolas Mar 16 '20 at 21:15
  • 1
    @MikeLui your comment is incorrect -- see my first comment under Ben's answer. For an object under construction, the behaviour is covered by class.cdtor, and not by the bullet points under basic.life/6 which are introduced by the word "Otherwise" – M.M Mar 16 '20 at 21:16
  • @M.M: in code executing concurrently with placement-new (as this question has), it is not known whether the object is preconstruction, under construction, or fully constructed. – Ben Voigt Mar 16 '20 at 21:17
  • @BenVoigt I don't want to repeat the discussion under your answer here . In Nicol's answer it addresses a hypothetical code where it is known that the object is under construction (see the third paragraph of this answer) – M.M Mar 16 '20 at 21:19
  • _"you have a data race due to the lack of synchronization between creation and use"_ I don't think this is called [data race](https://timsong-cpp.github.io/cppwp/n4659/intro.races#def:data_race). Because there is no read or modification here. – Language Lawyer Mar 17 '20 at 00:41
  • In all common arch, all bits of a ptr are used to represent the addr value, and you don't need to launder it, technically. (It practice, you do. But this is a LL q.) – curiousguy Mar 17 '20 at 18:42
  • @LanguageLawyer Lifetime race? – curiousguy Mar 17 '20 at 18:42
3

Besides the race condition (which you might be managing with mutexes or similar), you're subject to the usual limitations on an object whose lifetime has not yet started, namely:

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways.

See [basic.life] for the full list of operations that are and are not allowed.

In particular, one of the restrictions is that

The program has undefined behavior if:

...

  • the glvalue is used to call a non-static member function of the object

which clearly forbids your example.

Also [class.cdtor] says:

For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior

and even if you do synchronize to some event triggered after construction begins, this rule will forbid that code:

During the construction of an object, if the value of the object or any of its subobjects is accessed through a glvalue that is not obtained, directly or indirectly, from the constructor's this pointer, the value of the object or subobject thus obtained is unspecified

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • 1
    What about the "For an object under construction or destruction, see [class.cdtor]." part? That's kind of important here, since that's precisely the case under discussion. – Nicol Bolas Mar 16 '20 at 21:00
  • 2
    Immediately after your first quote, it says "For an object under construction or destruction, see [class.cdtor]. Otherwise, ". And your second quote is part of that "Otherwise" and therefore not applicable since we are talking about the case of an object under construction . In fact the case you lay out here would imply that no constructor can call any member function – M.M Mar 16 '20 at 21:01
  • 1
    @M.M: There is no happens-before relationship between the construction and the member function call, so the object is not guaranteed to be under construction. – Ben Voigt Mar 16 '20 at 21:03
  • 2
    @BenVoigt: Then it's UB because of a data race, so this section doesn't even apply. – Nicol Bolas Mar 16 '20 at 21:03
  • @NicolBolas: Covered in the first parenthetical of my answer, which was there in the very first version. – Ben Voigt Mar 16 '20 at 21:04
  • This is a bit confusing to me because according to [basic.life]: this is undefined before the lifetime of `o` starts, where the lifetime begins when "initialization" is complete. Initialization is vague and points to [dcl.init], which says an object may be default initialized, where the default constructor may be called. The default constructor is able to call member functions as per [class.cdtor]. So it is both defined and undefined, depending on what "initialization" means. Can you clarify what initialization means in [basic.life]? Apologies for lack of links in comment – Mike Lui Mar 16 '20 at 21:05
  • @MikeLui: Initialization is complete when the first non-delegating constructor of the most derived class finishes. I'll look for a reference for that. – Ben Voigt Mar 16 '20 at 21:06
  • @MikeLui: Initialization is described by the entire section [dcl.init]. This is not a short section. – Nicol Bolas Mar 16 '20 at 21:07
  • 1
    @BenVoigt If the constructor has started but not finished, then the object is under construction . This is inextricably linked to the race condition; if you say "besides the race condition" then the following text can only apply to the situation of the race happening to fall out such that the constructor had started. IOW I agree that the quoted text prevents calling a member function before the constructor has started, but that is already covered by pointing out the race condition – M.M Mar 16 '20 at 21:08
  • @M.M: What if instead of passing these two lambdas directly to `std::thread`, a wrapper is used that takes a global lock? Then the two function calls cannot overlap, preventing the data race... but you still do not know which happens first. There is no guarantee here that construction has begun, and providing that requires stronger synchronization than merely resolving the data race. – Ben Voigt Mar 16 '20 at 21:12
  • @BenVoigt: But then you're talking about a hypothetical scenario, not the given code. And that hypothetical scenario wouldn't even be addressing the question which is *specifically* about the period during construction. – Nicol Bolas Mar 16 '20 at 21:13
  • @NicolBolas: Maybe the mutex is released temporarily during construction (e.g. by waiting on a condition variable). Then the other thread could execute mid-construction with no data race. What I'm saying is that adding a mutex to solve the data race is not enough. The question specifically said to not worry about the data race which tells me that I can rely only on the minimum necessary changes to remove the data race, and not on any other change (like strong ordering between beginning of construction and the member function call) – Ben Voigt Mar 16 '20 at 21:16
  • @BenVoigt: "*this rule will forbid that code:*" That's interesting, because C++17 [doesn't have that rule](https://timsong-cpp.github.io/cppwp/n4659/class.cdtor). – Nicol Bolas Mar 16 '20 at 21:24
  • Switched the "marked answer" because I think the parts referenced in the above answer, while very helpful, aren't as specific to the scenario in my question. The redirection to [class.cdtor] means that the above quotes are less relevant. – Mike Lui Mar 16 '20 at 21:26
  • @NicolBolas _"Then it's UB because of a data race"_ There is no data race here. By [the definition of data race](https://timsong-cpp.github.io/cppwp/n4659/intro.races#def:data_race). – Language Lawyer Mar 17 '20 at 00:44