C++ Logo

std-discussion

Advanced search

Re: UB in P2641 'Checking if a union alternative is active'

From: Matthew House <mattlloydhouse_at_[hidden]>
Date: Thu, 22 Jun 2023 13:09:59 -0400
On Wed, Jun 21, 2023 at 9:29 PM Brian Bi via Std-Discussion
<std-discussion_at_[hidden]> wrote:
> On Tue, Jun 20, 2023 at 11:58 AM Matthew House via Std-Discussion <std-discussion_at_[hidden]> wrote:
>> Of course. I'm not trying to dispute the general idea that the pointer
>> type can be distinct from the dynamic type of the object. Instead, I'm
>> disputing that scalar accesses (lvalue-to-rvalue conversions and
>> assignments) in particular are and ought to be beholden to the dynamic
>> type of the object, since otherwise [basic.lval]/11 is a nearly
>> meaningless clause. Indeed, given the subject, I'm surprised that the
>> paper doesn't mention the clause at all, even to explain how it
>> doesn't help.
>
> Clearly, [basic.lval]/11 was meant to allow accessing of object representations. It's just that the current wording unfortunately does not actually yield the desired result.
>
> You seem to be pointing out that the interpretation of "accessing the `int` object through a `char` glvalue actually gives the `int` value, which immediately overflows and causes UB" is ridiculous. We all agree that it's ridiculous, but it is what the wording literally says right now.

The 'literally' part isn't so clear to me; I still think that "the
value contained in the object" is overly vague. Although looking at it
again, perhaps the 'ridiculous' reading would be better justified by
the first part, "the object indicated by the glvalue is read (3.1)",
pointing to the definition of "access" as "read or modify the value of
an object". And meanwhile, [expr.ass]/2 is pretty clear with "the
object referred to [...] is modified (3.1) by replacing its value".

> Also, acknowledging that it's ridiculous does not mean that we suddenly know what is the "intended" specification, where we can use `char` glvalues to access object representations.

We seem to agree that [basic.lval]/11 is meant to enable this, given
that its counterpart is precisely what enables accessing the object
representation in C. (Well, the first byte in the object
representation, at any rate; pointer arithmetic is still left UB
without a real array object. That also has to be fixed at some point.)

So I wouldn't find it much of a stretch to claim that access through
glvalues with byte types (i.e., char, unsigned char, or std::byte) is
similarly 'intended' to allow access to the object representation.
After all, if it were explicitly intended to bar such accesses, then
[basic.lval]/11 would look extremely different from its current form.
That's why I want to see the first half of this issue as a mere
oversight in [conv.lval]/(3.4) and [expr.ass]/2, leaving only the
inability to use pointer arithmetic to be solved.

> Under the current wording, only objects' values can be accessed; object representations cannot (unless you do it indirectly, i.e., `memcpy` into an array of `char`, `unsigned char`, or `std::byte` and then look at that). That's the problem that P1839 is trying to solve.
>
>> (And the paper's proposal would not fully solve the meaninglessness
>> of [basic.lval]/11 that it infers: by its logic, reinterpreting a
>> negative integer as an unsigned integer via type punning is instant
>> UB, compared to the analogous wording in C, where the relevant details
>> of the object representation are merely implementation-defined.)
>
> There is unfortunately no consensus about whether or not that should actually be allowed, but I'm hoping someone will write a separate paper at some point to address that issue.

If I have been unclear, I have absolutely no qualms with trying to
close this hole in the spec. I'm mainly concerned that P1839's
solution as written will enshrine the 'ridiculous' reading of
[conv.lval]/(3.4) and [expr.ass]/2, leaving us with a [basic.lval]/11
riddled with holes. In my view, if the behavior people would really
prefer is that the value accessed via the glvalue must be exactly the
value of the underlying object, then it would be better to throw out
[basic.lval]/11 entirely, given that it would only work for certain
positive values and be extremely contextual in its permitted use.

(I've always found the rule and its C counterpart to be misunderstood;
P1839 seems to repeat the popular misconception that the initial cast
is something which must not "violate the rules for type punning". So
perhaps that's the implicit reference to the rule that I was looking
for. But that rule has never been about pointer casts! Nothing stops
you from casting any object pointer type to any other object pointer
type, as long as the alignment requirement is met and it's not a
conversion between base and derived classes. Instead, the rule has
always been about giving the user liberty to access an object using a
different glvalue type, and if standards people don't actually want
that anymore, then the rule should be thrown out.)

Conversely, if the behavior of reinterpreting the object
representation is to be made explicit, then I feel that P1839's
solution, adding additional real objects to the object tree and using
nondeterminism to paper over the discrepancies, is more heavyweight
than it needs to be to solve the problem of pointer arithmetic; thus
my extended "modest proposal" from earlier.

Finally, as a brief note, P1839's proposed wording also seems to have
a couple issues apart from the known issues it mentions. For one, it
says that the object representation is an array object if the object
occupies contiguous bytes, but this would imply that each subobject
gets its own array object, and I don't see how that fits with its
being a subsequence of the full object's sequence. Also, it says that
the elements of the object representation are completely unspecified,
but that's inconsistent with the fact that at least some of the bits
must correspond to the value representation, and integer types have
certain guarantees regarding those.

Received on 2023-06-22 17:10:11