1

Regarding the following code:

class One { 
public:
  double number{};
};

class Two {
public:
  int integer{};
}

class Mixture {
public:
  double& foo() {
    new (&storage) One{1.0};
    return reinterpret_cast<One*>(&storage)->number;
  }

  int& bar() {
    new (&storage) Two{2};
    return reinterpret_cast<Two*>(&storage)->integer;
  }

  std::aligned_storage_t<8> storage;
};

int main() {
  auto mixture = Mixture{};
  cout << mixture.foo() << endl;
  cout << mixture.bar() << endl;
}

I haven't called the destructor for the types because they are trivially destructible. My understanding of the standard is that for this to be safe, we would need to launder the pointer to storage before passing it to the reinterpret_cast. However, std::optional's implementation in libstdc++ does not seem to use std::launder() and simply constructs the object right into the union storage. https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/optional.

Is my example above well defined behavior? What do I need to do to make it work? Would a union make this work?

timrau
  • 22,578
  • 4
  • 51
  • 64
Curious
  • 20,870
  • 8
  • 61
  • 146
  • I do not have an answer, but keep in mind that libstdc++ is not bound by the same rules as normal code. It can easily use things that would be undefined behavior in user code - because they are defining undefined behavior. – SergeyA Feb 07 '19 at 20:32
  • 2
    When using placement `new` you need to use the returned pointer. Do not cast the original pointer to the new type. – François Andrieux Feb 07 '19 at 20:36
  • @FrançoisAndrieux I think I understand that. Technically speaking, std::launder would need to be used here to avoid the need to store a pointer. Not sure if the code as a whole without launder is OK though. Since libstdc++ does it. – Curious Feb 07 '19 at 20:37
  • Why was this closed as a duplicate? The linked question asks about whether it is safe to completely avoid calling the destructor for a POD type. This question asks if we can reuse the memory for something else without calling the destructor for a trivially destructible type. – Curious Feb 07 '19 at 20:41
  • I've added to more targets that ask a more similar question. All the answers are the same though. – NathanOliver Feb 07 '19 at 20:47
  • 1
    @NathanOliver none of those mention whether we can avoid calling std::launder. In the case where the usecase had a buffer of a different type they used the pointed returned by placement new. That is not viable for my usecase. libstdc++ seems to do it without reusing the pointer. But I cannot justify their usage. Hence I asked. There might be some detail I’m missing with unions (note that I haven’t used a union here) Can we reopen this question? – Curious Feb 07 '19 at 20:50
  • 1
    Why aren't you just keeping the pointer returned by new? Then you don't need launder at all. – NathanOliver Feb 07 '19 at 20:53
  • The reason I’m doing this trickery is that I don’t have the space for the pointer. Those 8 bytes are too much :/ having the data fit in a cacheline is extremely valuable – Curious Feb 07 '19 at 20:53
  • 1
    `Mixture` doesn't need to hold the pointer. `static One* one = new (&storage) One{1.0}; return one->number;` would work. – NathanOliver Feb 07 '19 at 20:55
  • I cannot use function local statics.. This is ephemeral storage. Metadata cannot be used because this has no other storage capacity – Curious Feb 07 '19 at 20:57
  • 1
    That said I'll be happy to reopen the question if we remove the asking about *Is it safe to use the storage buffer space used for a trivially destructible class object for another object after using placement new on it?* and just asking if we need launder since the first question has already been asked and answered before. – NathanOliver Feb 07 '19 at 20:57
  • @NathanOliver done. Thanks – Curious Feb 07 '19 at 21:00
  • 1
    Is the question about reusing memory for trivial destructable objects or is it about if you need to use `std::launder`? Those appear to be entirely unrelated questions to me. – Mooing Duck Feb 07 '19 at 21:07
  • @MooingDuck I guess both? Reusing memory might require laundering based on how it's done, whether or not eliding laundering at all for trivially destructible types might be safe and well defined, even if libstdc++ does it for both cases (trivially destructible or not). Whether or not trivially destructible types need a destructor to be invoked can be considered unrelated, but the former requires the latter as it is one of the possible implementations of the former. However, please feel free to edit the question or suggest edits so I can make it more palatable. – Curious Feb 07 '19 at 21:11
  • 2
    @Curious Please don't ask "why the downvote? in comments. If someone downvoted they considered your question a waste of time, so why would they come back and notice your "why the downvote?" comment. And answering "why the downvote?" questions in the rare situation one does come back almost uniformly results in a crappy experience for the person who clarifies. To be more clear, if an OP has asked "why the downvote?" and I consider it worth another downvote, I'm strictly less likly to explain than if they hadn't. – Yakk - Adam Nevraumont Feb 07 '19 at 21:18
  • 1
    Have an upvote to balance things out @Curious – Mark Ingram Feb 07 '19 at 21:23
  • @Yakk-AdamNevraumont Two things - 1) I removed that comment before you made yours (There is no history feature that I know of, so either believe this or don't) , so I assume your comment is unnecessary now? Not sure why you decided to comment afterwards anyway. Perhaps the delete didn't propagate.... – Curious Feb 07 '19 at 21:31
  • 1
    @Yakk-AdamNevraumont And 2) I haven't really found that question to be a waste of time. In the past, it has led to the downvoter explaining themselves and usually me editing my code to be more appropriate. If someone considers a question a waste of time, usually someone has explained why the question was low-quality in the comments already, in this case I am not sure where the lack of quality comes from, so I asked. I guess your experience has not been quite the same? Regardless, not sure if this is a situation where you can prescribe one approach for all cases. – Curious Feb 07 '19 at 21:31
  • Why not use a simple `union`? – balki Feb 08 '19 at 00:36
  • @balki If a union makes this well defined behavior I would be willing to use that. I am not sure whether either is well defined. Edited the question to explicitly say that. – Curious Feb 08 '19 at 01:00
  • You need to use launder on the result of reinterpret_cast. About libstdc++: it can do whatever it wants, it is considered part of the compiler. But it is more likely that launder currently doesn't do anything, it would be just a nop (for the case `std::optional`). If compiler starts making optimizations where launder is needed, I expect that launder will be used in libstdc++ as well. As balki suggests, I'd use union for your case, as it is a more clean solution (and can be used in constexpr contexts as well). – geza Feb 08 '19 at 09:43
  • @geza Could you elaborate more on why a union is a cleaner approach here? Does that need laundering? – Curious Feb 08 '19 at 15:19
  • @Curious: no, it doesn't need laundering. And union solution can be used in constexpr context. It means, for example, that you can create a constexpr constructor for Mixture. With the reinterpret_cast, you cannot have a constexpr constructor, as placement new is forbidden in a constexpr function. – geza Feb 08 '19 at 16:52

1 Answers1

1

In your code, you do need std::launder in order to make your reinterpret_cast do what you want it to do. This is a separate issue from that of re-using memory. According to the standard ([expr.reinterpret].cast]7), your expression

reinterpret_cast<One*>(&storage)

is equivalent to:

static_cast<One*>(static_cast<void*>(&storage))

However, the outer static_cast does not succeed in producing a pointer to the newly created One object because according to [expr.static.cast]/13,

if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible (6.9.2) with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.

That is, the resulting pointer still points to the storage object, not to the One object nested within it, and using it as a pointer to a One object would violate the strict aliasing rule. You must use std::launder to force the resulting pointer to point to the One object. Or, as pointed out in the comments, you could simply use the pointer returned by placement new directly, rather than the one obtained from reinterpret_cast.

If, as suggested in the comments, you used a union instead of aligned_storage,

union {
    One one;
    Two two;
};

you would sidestep the pointer-interconvertibility issue, so std::launder would not be needed on account of non-pointer-interconvertibility. However, there is still the issue of re-use of memory. In this particular case, std::launder is not needed on account of re-use because your One and Two classes do not contain any non-static data members of const-qualified or reference type ([basic.life]/8).

Finally, there was the question of why libstdc++'s implementation of std::optional does not use std::launder, even though std::optional may contain classes that contain non-static data members of const-qualified or reference type. As pointed out in comments, libstdc++ is part of the implementation, and may simply elide std::launder when the implementers know that GCC will still compile the code properly without it. The discussion that led up to the introduction of std::launder (see CWG 1776 and the linked thread, N4303, P0137) seems to indicate that, in the opinion of people who understand the standard much better than I do, std::launder is indeed required in order to make the union-based implementation of std::optional well-defined in the presence of members of const-qualified or reference type. However, I am not sure that the standard text is clear enough to make this obvious, and it might be worth having a discussion about how it might be clarified.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312