Is it defined behavior to explicitly call a destructor and then use placement new to reconstruct it?

Viewed 304

This is very similar to Correct usage of placement-new and explicit destructor call but tighter in scope:

If I have a type, S, for which std::is_nothrow_default_constructible_v<S> and std::is_nothrow_destructible_v<S> and not std::has_virtual_destructor_v<S> and not std::is_polymorphic_v<S>, is it defined behavior to call this function?:

template <typename T>
void reconstruct(T& x) noexcept {
    // C++20: requires instead of static_assert:
    static_assert(std::is_nothrow_default_constructible_v<T>);
    static_assert(std::is_nothrow_destructible_v<T>);
    static_assert(!std::has_virtual_destructor_v<T>);
    static_assert(!std::is_polymorphic_v<T>);
    x.~T();
    ::new (&x) T{};
}

What if there are existing pointers or references to, as in

int main() {
    S s;
    s.x = 42;
    const S& sref = s;
    reconstruct(s);
    return sref.x; // Is this UB because the original S sref references no longer exists?
}

My reason to ask this is that std::once_flag has no reset mechanism. I know why it generally can't and it would be easy to misuse, however, there are cases where I'd like to do a thread-unsafe reset, and I think this reconstruction pattern would give me what I want provided this reconstruction is defined behavior. https://godbolt.org/z/de5znWdYT

1 Answers

Unfortunately, sometimes it is defined behavior, and other times you have to run the pointer through std::launder. There are a number of cases where the compiler may assume that the object hasn't changed, particularly if there are references or const fields in struct S. More information is available in the cppreference section on storage reuse.

In the particular code that you show, you will run into problems if S is a base class of the complete object, because the compiler could, for example, be caching to wrong vtable somewhere.

In a bit more detail, this is something that has changed between C++17 and C++20. In C++17 and earlier, you need to call std::launder when S contains any const or reference fields. However, that particular requirement has been relaxed in C++20, though there are still other restrictions.

Related