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

1

13

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

Nilotic answered 7/7, 2022 at 18:38 Comment(20)
I would hope that has_virtual_destructor_v<S> == false as well.Anabelle
Doesn't happen in this source code, but this certainly gets problematic, if s is a instance of a subclass of S: There need not be a virtual destructor for S which could result in the object being improperly destroyed and "partially resurrected" as a different object type. Any access to functionality of the subtype in main would necessarily be UB.Hell
Isn't is a better solution to not use call_once if you want to call it more than once!? This smells like an XY-problem.Lame
Yes, it is defined. /threadGaskill
@Anabelle Good point. I'll add that to the question. I wasn't trying to get into a polymorphism tangent.Nilotic
@LanguageLawyer are there any gotchas with respect to callers? Is it defined behavior even if S contains const data (or a reference)?Nilotic
I think that the return sref.x would be UB if S were a trivial-layout struct with no constructor or member initializer -- it would be UB because it's uninitialized. However, I don't think you can call reconstruct this way, passing a const S & to a non-const reference parameter.Anabelle
@LanguageLawyer in particular, I was expecting this question to be related to std::launder and the world of placement-newing things in raw memory.Nilotic
OK, so I made reconstruct a template and everything compiled, so you should edit your question to correspond to that. I can't; the queue is full.Anabelle
At least before C++20, if S has any const or reference members then you can't use this technique.Auntie
Might want to sanity check against std::is_polymorphic, since reconstruct might not do the right thing for polymorphic types.Sophist
eel.is/c++draft/basic.life#8 "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 the original object is transparently replaceable by the new object"Anisotropic
The rule isn't new in C++20, although the terminology of "transparently replaceable" is. And the one rule forbade const and reference members in the object being replaced (because consumers of the object "know" those cannot be reassigned). The removal of this restriction was highly backward-incompatible and even broke major parts of the Standard library (which are getting fixed, presumably, while user-written code is likely to rot)Anisotropic
@BenVoigt That's interesting, I did not realize there are potential backwards-breaks for user-written code due to this change (US041 of P2103R0, I guess?). Could you give an example?Almeta
@Almeta const member restriction was dropped in RU007Lacylad
@dfrib: For example, std::unordered_map relied on that restriction via std::pair<const key_type, value_type> to guarantee that keys didn't change hash values while inside the container (which would result in never being able to find them). Third-party hashtables, caching layers, etc will likewise have the rug yanked out from under them. The incompatibility is not a change in the behavior of valid say C++14 code, it's inability to compose code written for C++14 with new C++20 code.Anisotropic
@BenVoigt you are saying libraries will "have the rug yanked out from under them" in that someone could reconstruct the value at &*umap.find(k)? I understand why the key part of the std::par has to be const but how does the rule-change affect it? What was valid in C++14 but breaks in 20?Nilotic
@Ben: Yes, that's correct. Obviously it's more likely to occur if there are several more layers between obtaining a reference to the std::pair<const key_type, value_type> and the code doing the reconstruct. I'll repeat the takeaway: The incompatibility is not a change in the behavior of valid say C++14 code, it is inability to safely compose code valid when written for C++14 with new valid C++20 code.Anisotropic
I talked to some committee members about the issue, and their "solution" was that the third party should have copied the restriction into their own documentation... which doesn't seem practical without a time machine, since at the time the library was written that restriction was provided by the language, and I think it's very unrealistic to expect library authors to copy large chunks from all areas of the C++ specification into their documentation in anticipation of arbitrary and unpredictable future changes to the language rules.Anisotropic
@Ben: Also note that while the third-party code was valid and correct when written for C++14, it is no longer considered correct even for C++14, because RU007 was accepted as a retroactively-effective defect report. Madness!Anisotropic
C
4

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.

Chickie answered 7/7, 2022 at 23:16 Comment(2)
Thank you. This answers the question, addresses the subtleties I was curious about, and the "storage reuse" link is the detailed reading I was looking for.Nilotic
The part about const and reference members isn't relevant anymore since C++20. But the requirement that the element be not potentially-overlapping (e.g. a base subobject) is still relevant. Also I don't think that a std::launder will enough to make it safe in the latter case, since e.g. padding might have been reused so that the storage doesn't exactly overlap.Scalp

© 2022 - 2024 — McMap. All rights reserved.