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 05:01:06 UTC 2025
On Tue, 22 Apr 2025 04:52:45 GMT, Quan Anh Mai <qamai at openjdk.org> wrote:
>> 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, b...
>
> 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.
To be perfectly clear, we can just remove launder from the patch, and do not need to do this:
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)...);
}
Just do it normal:
template<typename... Args>
void initialize(Args&&... args) {
assert(!_initialized, "already initialized");
::new (&_obj) T(std::forward<Args>(args)...);
}
-------------
PR Review Comment: https://git.openjdk.org/jdk/pull/24689#discussion_r2053330459
More information about the hotspot-dev
mailing list