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

Ben
  • 9,184
  • 1
  • 43
  • 56
  • 3
    I would hope that `has_virtual_destructor_v == false` as well. – Spencer Jul 07 '22 at 18:47
  • 2
    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. – fabian Jul 07 '22 at 19:06
  • 7
    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. – BoP Jul 07 '22 at 19:11
  • 2
    Yes, it is defined. /thread – Language Lawyer Jul 07 '22 at 19:17
  • @Spencer Good point. I'll add that to the question. I wasn't trying to get into a polymorphism tangent. – Ben Jul 07 '22 at 19:18
  • @LanguageLawyer are there any gotchas with respect to callers? Is it defined behavior even if `S` contains `const` data (or a reference)? – Ben Jul 07 '22 at 19:19
  • 1
    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. – Spencer Jul 07 '22 at 19:26
  • @LanguageLawyer in particular, I was expecting this question to be related to `std::launder` and the world of placement-`new`ing things in raw memory. – Ben Jul 07 '22 at 19:33
  • 1
    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. – Spencer Jul 07 '22 at 19:36
  • 2
    At least before C++20, if `S` has any const or reference members then you can't use this technique. – NathanOliver Jul 07 '22 at 19:53
  • 1
    Might want to sanity check against [`std::is_polymorphic`](https://en.cppreference.com/w/cpp/types/is_polymorphic), since `reconstruct` might not do the right thing for polymorphic types. – Eljay Jul 07 '22 at 20:25
  • 2
    https://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" – Ben Voigt Jul 07 '22 at 21:46
  • 2
    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) – Ben Voigt Jul 07 '22 at 21:48
  • @BenVoigt That's interesting, I did not realize there are potential backwards-breaks for user-written code due to this change (US041 of [P2103R0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2103r0.html), I guess?). Could you give an example? – dfrib Jul 08 '22 at 08:30
  • @dfrib const member restriction was dropped in [RU007](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1971r0.html#RU007) – Cubbi Jul 08 '22 at 14:49
  • 2
    @dfrib: For example, `std::unordered_map` relied on that restriction via `std::pair` 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. – Ben Voigt Jul 08 '22 at 15:24
  • @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? – Ben Jul 08 '22 at 16:11
  • 1
    @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` 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.** – Ben Voigt Jul 08 '22 at 16:14
  • 2
    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. – Ben Voigt Jul 08 '22 at 16:17
  • 1
    @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! – Ben Voigt Jul 08 '22 at 16:20

1 Answers1

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.

user3188445
  • 4,062
  • 16
  • 26
  • 1
    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. – Ben Jul 08 '22 at 11:05
  • 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. – user17732522 Jul 08 '22 at 18:27