C++ Logo

SG12

Advanced search

Subject: Re: [ub] type punning through congruent base class?
From: David Krauss (david_work_at_[hidden])
Date: 2014-01-17 01:56:11


On Jan 17, 2014, at 3:38 AM, Herb Sutter <hsutter_at_[hidden]> wrote:

> Richard, it cannot mean that (or if it does, IMO we have an obvious bug) for at least two specific reasons I can think of (below), besides the general reasons that it would not be sensical and would violate type safety.
>
> First, objects must have unique addresses. Consider, still assuming B is trivially constructible:
> void *p = malloc(sizeof(B));
> B* pb = (B*)p;
> pb->i = 0;
> short* ps = (short*)p;
> *ps = 0;
> This cannot possibly be construed as starting the lifetime of a B object and a short object, else they would have the same address, which is illegal. Am I missing something?

1.8/6 applies here:

Two objects that are not bit-fields may have the same address if one is a subobject of the other,

Even without that, it’s not clear what the meaning of a POD object is. You can arbitrarily say that the lifetimes of two aliased objects begin and end however you like, as the lifetime of a complete object does not seem to dictate the lifetime of its subobjects.

struct q { int m; };
struct r { int n; };

static_assert ( sizeof (q) == sizeof (int), “” );

int main() {
    q * pq = (q *) malloc( sizeof (int) ); // Nothing here implies a lifetime.
    * (int *) pq = 5; // Begin lifetime of int object.
    r * pr = (r *) pq; // I decree that an r comes to life here.
    std::cout << pr->n << ‘\n’; // The int is still alive, though.
    // OK, now I want the r lifetime to end and a q lifetime to begin.
    // No normative rule says not, because storage is already obtained.
    // The int continues to live on as initialized.
    std::cout << pq->m << ‘\n’;
}

The catch (or saving grace) is that the reinterpret_cast only works for the first subobject of a standard layout class or an array. Although, I don’t see why you couldn’t just assert that the offsets or member subobject pointer values match (as void*) in other cases.

Even if the lifetime of an object is passed on to its subobjects, the object representation must be shared by the ints and that guarantees identical values, or if you prefer, pre-initialization.

> The more I see about this, the more I think the lifetime wording (a) is correct for non-trivial-ctor cases and (b) is incomplete or incorrect for trivial-ctor cases.
> In particular, untyped storage (such as returned from malloc) should never be considered of any type T until after the possibly-trivial T::T has run, such as via placement new. Note I include “possibly-trivial” there on purpose. If you don’t call a T ctor, you don’t have a T object – that has to be true. Otherwise don’t we have multiple contradictions in the standard?

C++ conspicuously does not require a new-expression to begin the lifetime of a trivially-constructible class. I’ve always assumed this to be to bless malloc-based C code, and by extension libraries written in C. So, what meaning can be assigned to the constructor?

The current rules seem to be very loose, but the loophole should be tightened very carefully because a lot of code must depend on it.

I’m not sure whether my example above should be deprecated, but if so, there might be something about a minimal lifetime for an object, or some sort of end-of-lifetime event that is required between “aliased" uses of the same storage as different class types, which conceptually taints the object representation.

I wrote an earlier post to this thread about aliasing and lvalue-to-rvalue conversion but it wasn’t distributed (or bounced) by the listserv. Hopefully this message fares better.



SG12 list run by herb.sutter at gmail.com