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
char
andbool
here. Is this allowed, even though thechar
is a complete object and does not provide storage for thebool
inside? - Even if strict aliasing isn't violated, does
c
in itself remain valid given that the lifetime of thechar
ended and given that it doesn't provide storage? - Does the cast to
bool&
not requirestd::launder
, given that the provenance of the obtained reference does not lead back to abool
object?
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 wholeOptBool
object, although I am not sure that this is currently well-specified when it happens in the constructor. The newly-createdbool
object can't become a member subobject because it doesn't have the same type asc
and therefore can't be nested within theOptBool
object. The lifetime of an object ends if its storage is reused by an object not nested within it. This can be fixed by makingc
aunsigned char[1]
which can provide storage for thebool
object.With that change:
If
OptBool(bool b)
was used, then there is abool
object at the address of&c[0]
. However,std::launder
is required to get a pointer to thisbool
object from the pointer to theunsigned char
object, 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_value
in that situation. Withnew
the lifetime of theunsigned char
object ended because its storage is reused. Thereforec[0] != 2
performs a read out-of-lifetime, which is UB.In order to get a pointer to the
bool
object, 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
bool
object, 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::launder
requires abool
object to be alive at the address. I don't think that can be fixed.Whether or not it was intended that
std::launder
is 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.