In P2641r4: Checking if a union alternative is active, the author provides an implementation of an optional<bool> as a motivating example and claims that this is well-formed.
struct OptBool { char c; OptBool() : c(2) { } OptBool(bool b) { new (&c) bool(b); } auto has_value() const -> bool { return c != 2; } auto operator*() -> bool& { return (bool&)c; } };
However, I am not convinced, and there are issues that raise my eyebrows:
- There is aliasing between
charandboolhere. Is this allowed, even though thecharis a complete object and does not provide storage for theboolinside? - Even if strict aliasing isn't violated, does
cin itself remain valid given that the lifetime of thecharended and given that it doesn't provide storage? - Does the cast to
bool¬ requirestd::launder, given that the provenance of the obtained reference does not lead back to aboolobject?
Note: This is a sister question to Is the author's union-based implementation of an optional<bool> well-defined in P2641? which discusses the other implementation.
I assume that
operator*has a precondition as usual that theOptBool(bool b)overload has been used. Usingoperator*when the optional is empty is clearly UB, but also is not intended use.The first problem is that
new (&c) bool(b);will end the lifetime of the wholeOptBoolobject, although I am not sure that this is currently well-specified when it happens in the constructor. The newly-createdboolobject can't become a member subobject because it doesn't have the same type ascand therefore can't be nested within theOptBoolobject. The lifetime of an object ends if its storage is reused by an object not nested within it. This can be fixed by makingcaunsigned char[1]which can provide storage for theboolobject.With that change:
If
OptBool(bool b)was used, then there is aboolobject at the address of&c[0]. However,std::launderis required to get a pointer to thisboolobject from the pointer to theunsigned charobject, because the objects are not pointer-interconvertible, nor transparently-replaceable (as they do not have the same type). So it can be fixed:With that fixed, there is however still a problem with
has_valuein that situation. Withnewthe lifetime of theunsigned charobject ended because its storage is reused. Thereforec[0] != 2performs a read out-of-lifetime, which is UB.In order to get a pointer to the
boolobject, but with expression typeunsigned char, one would have to do this:Then this isn't an aliasing violation. However, it is currently not specified what value this access should read. The intention is for it to read the first byte of the object representation of the
boolobject, but that isn't specified to happen at the moment. There is P1839 trying to fix that. It is in practice what everyone assumes as the behavior, even if the standard doesn't say that at the moment, which is a defect.However, if the optional is empty, then
std::launder(reinterpret_cast<bool*>(&c[0]))has undefined behavior becausestd::launderrequires aboolobject to be alive at the address. I don't think that can be fixed.Whether or not it was intended that
std::launderis required in this situation is another question, but that's how it seems to be specified right now.In any case, the implementation of course assumes a specific implementation of
bool, specifically its size, alignment and object/value representations.