C++ Logo

SG12

Advanced search

Subject: Re: [ub] type punning through congruent base class?
From: Richard Smith (richardsmith_at_[hidden])
Date: 2014-01-15 17:08:57


On Wed, Jan 15, 2014 at 1:48 PM, Herb Sutter <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_[hidden] <ub-bounces_at_[hidden]> on behalf of
> Richard Smith <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]> 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]
> http://www.open-std.org/mailman/listinfo/ub
>
>



SG12 list run by herb.sutter at gmail.com