Is the author's cast-based implementation of an optional<bool> well-defined in P2641?

119 Views Asked by At

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:

  1. There is aliasing between char and bool here. Is this allowed, even though the char is a complete object and does not provide storage for the bool inside?
  2. Even if strict aliasing isn't violated, does c in itself remain valid given that the lifetime of the char ended and given that it doesn't provide storage?
  3. Does the cast to bool& not require std::launder, given that the provenance of the obtained reference does not lead back to a bool 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.

1

There are 1 best solutions below

3
On

I assume that operator* has a precondition as usual that the OptBool(bool b) overload has been used. Using operator* 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 whole OptBool object, although I am not sure that this is currently well-specified when it happens in the constructor. The newly-created bool object can't become a member subobject because it doesn't have the same type as c and therefore can't be nested within the OptBool object. The lifetime of an object ends if its storage is reused by an object not nested within it. This can be fixed by making c a unsigned char[1] which can provide storage for the bool object.

With that change:

If OptBool(bool b) was used, then there is a bool object at the address of &c[0]. However, std::launder is required to get a pointer to this bool object from the pointer to the unsigned 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:

return *std::launder(reinterpret_cast<bool*>(&c[0]));

With that fixed, there is however still a problem with has_value in that situation. With new the lifetime of the unsigned char object ended because its storage is reused. Therefore c[0] != 2 performs a read out-of-lifetime, which is UB.

In order to get a pointer to the bool object, but with expression type unsigned char, one would have to do this:

*reinterpret_cast<unsigned char*>(std::launder(reinterpret_cast<bool*>(&c[0])))

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 because std::launder requires a bool 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.