Date: Thu, 16 Jan 2014 19:38:25 +0000
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?
Second, this function:
template<typename T> T* test() {
auto p = (T*)malloc(sizeof(T));
new (p) T{};
return p;
}
clearly returns a pointer to a T object for any default-constructible type T – and that has to be true whether or not T is trivially constructible.
If the current rules are as Richard says, then it would true that test() returns a pointer to a T, unless T is trivially constructible in which case it returns a pointer to anything of size T. That doesn’t make any sense, and if the rules say that now I’d say we have a defect.
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?
“It’s not a T until the T ctor runs” has to be part of the rules somewhere. It is there for non-trivially-constructible types.
Do we have a defect here that we’re missing that rule for trivially-constructible types?
Herb
From: ub-bounces_at_[hidden] [mailto:ub-bounces_at_[hidden]] On Behalf Of Richard Smith
Sent: Wednesday, January 15, 2014 4:52 PM
To: WG21 UB study group
Subject: Re: [ub] type punning through congruent base class?
On Wed, Jan 15, 2014 at 4:32 PM, Herb Sutter <hsutter_at_[hidden]<mailto:hsutter_at_[hidden]>> wrote:
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;
(blink) D is not even mentioned, so why would we expect that? Can you explain?
Sorry, editing error, I meant 'B' here.
If the standard said the lifetime of a D object started somewhere here, I would wonder if there’s a defect in the lifetime wording. That would seem to violate sensible notions of type safety.
Playing devil's advocate again: it would be entirely unobservable whether an object of type 'B' or 'D' actually had its lifetime start here. Why would you be surprised if you got a 'D' instead of a 'B'? Indeed, if later I wrote:
D *q = (D*)p; // ha ha, it was a D object the whole time!
int n = q->i;
... why should that /not/ work? How is this any different from if I cast to D* before I used the object as a B?
From: ub-bounces_at_[hidden]<mailto:ub-bounces_at_open-std.org> [mailto: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
_______________________________________________
ub mailing list
ub_at_isocpp.open-std.org<mailto:ub_at_[hidden]>
http://www.open-std.org/mailman/listinfo/ub
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?
Second, this function:
template<typename T> T* test() {
auto p = (T*)malloc(sizeof(T));
new (p) T{};
return p;
}
clearly returns a pointer to a T object for any default-constructible type T – and that has to be true whether or not T is trivially constructible.
If the current rules are as Richard says, then it would true that test() returns a pointer to a T, unless T is trivially constructible in which case it returns a pointer to anything of size T. That doesn’t make any sense, and if the rules say that now I’d say we have a defect.
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?
“It’s not a T until the T ctor runs” has to be part of the rules somewhere. It is there for non-trivially-constructible types.
Do we have a defect here that we’re missing that rule for trivially-constructible types?
Herb
From: ub-bounces_at_[hidden] [mailto:ub-bounces_at_[hidden]] On Behalf Of Richard Smith
Sent: Wednesday, January 15, 2014 4:52 PM
To: WG21 UB study group
Subject: Re: [ub] type punning through congruent base class?
On Wed, Jan 15, 2014 at 4:32 PM, Herb Sutter <hsutter_at_[hidden]<mailto:hsutter_at_[hidden]>> wrote:
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;
(blink) D is not even mentioned, so why would we expect that? Can you explain?
Sorry, editing error, I meant 'B' here.
If the standard said the lifetime of a D object started somewhere here, I would wonder if there’s a defect in the lifetime wording. That would seem to violate sensible notions of type safety.
Playing devil's advocate again: it would be entirely unobservable whether an object of type 'B' or 'D' actually had its lifetime start here. Why would you be surprised if you got a 'D' instead of a 'B'? Indeed, if later I wrote:
D *q = (D*)p; // ha ha, it was a D object the whole time!
int n = q->i;
... why should that /not/ work? How is this any different from if I cast to D* before I used the object as a B?
From: ub-bounces_at_[hidden]<mailto:ub-bounces_at_open-std.org> [mailto: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
_______________________________________________
ub mailing list
ub_at_isocpp.open-std.org<mailto:ub_at_[hidden]>
http://www.open-std.org/mailman/listinfo/ub
Received on 2014-01-16 20:38:53