RFR: 8354954: Typed static memory for late initialization of static class members in Hotspot [v4]
Quan Anh Mai
qamai at openjdk.org
Tue Apr 22 04:55:48 UTC 2025
On Mon, 21 Apr 2025 21:56:58 GMT, Kim Barrett <kbarrett at openjdk.org> wrote:
>> That additional indirection is what we already have with the current idiom. I'm looking for a way to be
>> safe while removing the "do we need to do laundry" question. But I'm currently thinking we don't need
>> to do laundry, though I'm still working through the details of the rationale, and still might end up deciding
>> I'm wrong.
>
> I think something this works, and that no laundering is required.
>
> template<typename T>
> class StableValue {
> union { T _obj; };
> DEBUG_ONLY(bool _initialized;)
> public:
> StableValue() DEBUG_ONLY(: _initialized(false)) {}
> ~StableValue() {}
> NONCOPYABLE(StableValue);
> T* ptr() {
> assert(_initialized, "uninitialized");
> return &_obj;
> }
> T* operator->() { return ptr(); }
> T& operator*() { return *ptr(); }
> template<typename... Args>
> void initialize(Args&&... args) {
> assert(!_initialized, "already initialized");
> using NCVP = std::add_pointer_t<std::remove_cv_t<T>>;
> ::new (const_cast<NCVP>(_obj)) T(std::forward<Args>(args)...);
> }
> };
>
> [Note that I've made a few other improvements, such as supporting a cv-qualified
> type and being non-copyable. Obviously there's lots of missing comments...]
>
> The rationale for not needing laundering is as follows.
>
> std::launder is introduced to work around some restrictions involving reuse of
> storage for a different object. Specifically, C++17 6.8.8 (Object lifetime)
> says
>
> --- begin quote ---
>
> If, after the lifetime of an object has ended and before the storage which the
> object occupied is reused or released, a new object is created at the storage
> location which the original object occupied, a pointer that pointed to the
> original object, a reference that referred to the original object, or the name
> of the original object will automatically refer to the new object and, once
> the lifetime of the new object has started, can be used to manipulate the new
> object, if:
>
> [... list of requirements ...]
>
> [Note: Note: If these conditions are not met, a pointer to the new object can
> be obtained from a pointer that represents the address of its storage by
> calling std::launder. -- end note.]
>
> --- end quote ---
>
> Other than it's definition and an example, that's the only occurrence of
> "launder" in C++17.
>
> But if we're careful, that section doesn't apply at all. By using the
> uninitialized anonymous union trick (or a similarly uninitialized and
> appropriately aligned and sized char array), we have storage but not an object
> whose lifetime has ended. There wasn't a prior object in that storage.
>
> Instead we're dealing with storage either before or after the object's
> lifetime has started. There are restrictions on what one can do with a pointer
> to storage before the lifetime of the object starts, but we're not violating
> any of those. The object's lifetime starts with the placement-new.
I believe you are right, we do not need launder here. From [[class.union]/6](https://eel.is/c++draft/class.union#6):
> [Note [5](https://eel.is/c++draft/class.union#general-note-5): In cases where the above rule does not apply, the active member of a union can only be changed by the use of a placement [new-expression](https://eel.is/c++draft/expr.new#nt:new-expression)[.](https://eel.is/c++draft/class.union#general-6.sentence-1) — end note]
[Example [3](https://eel.is/c++draft/class.union#general-example-3): Consider an object u of a union type U having non-static data members m of type M and n of type N[.](https://eel.is/c++draft/class.union#general-6.sentence-2) If M has a non-trivial destructor and N has a non-trivial constructor (for instance, if they declare or inherit virtual functions), the active member of u can be safely switched from m to n using the destructor and placement [new-expression](https://eel.is/c++draft/expr.new#nt:new-expression) as follows:
u.m.~M();
new (&u.n) N;
— end example]
We can see that `::new(_&obj) T()` is considered an act to make `_obj` active, not to create a new `T`, the `_obj` itself has been created before at this location, it is just that it has not been initialized and its lifetime has not started. As a result, `_obj` can be used to refer to this object without launder.
-------------
PR Review Comment: https://git.openjdk.org/jdk/pull/24689#discussion_r2053325889
More information about the hotspot-dev
mailing list