C++ Logo

sg12

Advanced search

Re: [ub] type punning through congruent base class?

From: Gabriel Dos Reis <gdr_at_[hidden]>
Date: Wed, 15 Jan 2014 23:40:41 +0000
I suspect I would have to strongly disagree with the model put forward. Even if we attempted to make C++ look more like C, I don’t see anything that would make p->i = 0; starts the lifetime of a D object.

It has been the very foundation of C++ since day 1 that an object’s lifetime starts after its constructors ends and ends right before its destructor starts. Then, there was a little special case made for C. In my view, the way to resolve these questions isn’t to make C++ look more like C; it is to make the exceptions closer to the founding principle.

-- Gaby

From: ub-bounces_at_[hidden] [mailto:ub-bounces_at_[hidden]] On Behalf Of Richard Smith
Sent: Wednesday, January 15, 2014 3:09 PM
To: WG21 UB study group
Subject: Re: [ub] type punning through congruent base class?

On Wed, Jan 15, 2014 at 1:48 PM, Herb Sutter <hsutter_at_[hidden]<mailto:hsutter_at_[hidden]>> wrote:

Richard, I'm not sure I understand your position... Given the following complete program ...


  struct B { int i; };
  struct D : B { };

  int main() {
    B b; // line X
  }

... are you actually saying that line X starts the lifetime of an object of type D? or just setting up a strawman? (Sorry, I really couldn't tell.)

If yes, then given the following complete program ...

  struct C { long long i; };

  int main() {
    C c; // line Y
  }

... are you saying that line Y could start the lifetime of an object of type D (which is not mentioned in the code), double, shared_ptr<widget>, or any other type than C, as long as the size of that other type is the same or less than sizeof(C)?

I think my position is more nuanced. There are a set of cases that I think people expect to have defined behavior, and the standard does not currently distinguish those cases from your cases above, as far as I can tell -- if using malloc to create an object "with trivial initialization" is acceptable, then it appears that *any* way of producing the appropriately-sized-and-aligned storage is acceptable.


I would expect that that we would have consensus that the lifetime of an object of type D *should* start at some point in this code (and more specifically that the code has defined behavior):

  B *p = (B*)malloc(sizeof(B));
  p->i = 0;

I would expect to also have consensus that the same is true here:

  alignof(B) char buffer[sizeof(B)];
  B *p = (B*)buffer;
  p->i = 0;

(These expectations are based on the vast amount of existing code that relies upon these behaviors, not on the wording of the standard.)

So, that's "what people probably expect". Next, "what the standard says". Consider 3.8/1:

"The lifetime of an object of type T begins when:
— storage with the proper alignment and size for type T is obtained, and
— if the object has non-trivial initialization, its initialization is complete."

This requires us to answer three questions: (1) is there an object of type B in the above snippets, (2) when is storage for it obtained, and (3) does it have non-trivial initialization? It seems that "yes, before the storage is used as an object of type B, no" is a self-consistent set of answers, and one that gives the program defined behavior. There are also sets of answers that give the program undefined behavior, and the standard doesn't give us direction in how we might pick a set of answers to these questions.

There seem to be two obvious ways forward: either (a) if we can pick answers to these questions such that the program has defined behavior, then the program has defined behavior, or (b) if we can pick answers to these questions such that the program has undefined behavior, then the program has undefined behavior.

If we want to align "what people probably expect" with "what the standard says", it seems we need to either change the above rule, or accept interpretation (a), under which the program above is valid, as is any other program where 'buffer' obtains storage of the appropriate size and alignment for an object of type D. (Option (a) also matches the behavior of current optimizing compilers, as far as I'm aware.)


I've spent quite some time thinking about and discussing this problem and related issues (such as, under what circumstances does a pointer point to an object, when do two pointers alias, ...), and I personally think that the best approach here is to embrace option (a) above: if there exist a consistent set of choices of object lifetimes such that the program has defined behavior, then the program has the behavior implied by that set. (I have a sketch proof that such behavior is the same for *every* such consistent set, aside from deviations caused by unspecified values and other pre-existing sources of nondeterminism.) In essence, the implication of this is that objects' lifetimes would start just in time to avoid undefined behavior.

Put another way, yes, I personally think this code should have defined behavior:

C c; // #1
static_assert(sizeof(C) >= sizeof(D) && alignof(C) >= alignof(D), "");
D *p = (D*)&c;
d->i = 0; // #2

... and the lifetime of a D object at address &c should start at some point between lines #1 and #2. (Naturally, the lifetime of the C object ended before this happened.) Moreover, this is something that plenty of existing C++ code relies on.

From: ub-bounces_at_open-std.org<mailto:ub-bounces_at_[hidden]> <ub-bounces_at_[hidden]<mailto:ub-bounces_at_[hidden]>> on behalf of Richard Smith <richardsmith_at_[hidden]<mailto:richardsmith_at_[hidden]>>
Sent: Monday, January 6, 2014 3:44 PM

To: WG21 UB study group
Subject: Re: [ub] type punning through congruent base class?

On Mon, Jan 6, 2014 at 10:22 AM, Jason Merrill <jason_at_[hidden]<mailto:jason_at_[hidden]>> wrote:
On 01/06/2014 04:26 AM, Fabio Fracassi wrote:
> if it is not (legal): could we make it legal or would we run afoul of
> the aliasing rules?
The access is not allowed by the aliasing rules in 3.10. But it seems
that this would be:

struct B {
   int i;
};

struct D {
   B bmem;
   void foo() { /* access bmem.i */ }
};

B b;
reinterpret_cast<D&>(b).foo();

because B is a non-static data member of D, and 9.2/19 guarantees that
the address of D::bmem is the same as the address of the D object.

How is that fundamentally different? 9.3.1/2 makes that UB too, if 'reinterpret_cast<D&>(b)' does not refer to an object of type 'b'. And within D::foo, the implicit this->bmem would have the same problem.


If I might play devil's advocate for a moment...

  struct B { int i; };
  struct D : B {
    void foo();
  };

  B b;

I claim this line starts the lifetime of an object of type D. Per [basic.life]p1, the lifetime of an object of type 'D' begins when storage with the proper alignment and size for type T is obtained (which "B b" happens to satisfy). The object does not have non-trivial initialization, so the second bullet does not apply.

(This is the same argument that makes this valid:

  D *p = (D*)malloc(sizeof(D));
  p->foo();

... so any counterargument will need to explain why the two cases are fundamentally different.)

Then:

  reinterpret_cast<D&>(b).foo();

... is valid, because the cast produces the same memory address, and that memory address contains an object of type 'D' (as claimed above).

_______________________________________________
ub mailing list
ub_at_[hidden]<mailto:ub_at_[hidden]>
http://www.open-std.org/mailman/listinfo/ub

Received on 2014-01-16 00:40:47