Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Trivial initialization after non-trivial destruction

79 views
Skip to first unread message

Nikolay Ivchenkov

unread,
May 10, 2012, 2:48:28 PM5/10/12
to
Consider the following example:

struct X
{
~X() {}
};

template <class T>
void destroy(T &x)
{ x.~T(); }

int main()
{
X *p = (X *)operator new(sizeof(X));
destroy(*p);
destroy(*p); // well-defined or undefined?
operator delete(p);
}

According to C++11 - 3.8/1, non-trivial destruction ends the life-time
of an object. Can we assume that a new object of the same type exists
at the same location immediately after such non-trivial destruction
has done if its initialization is trivial?


--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]

Daniel Krügler

unread,
May 10, 2012, 5:30:06 PM5/10/12
to
Am 10.05.2012 20:48, schrieb Nikolay Ivchenkov:
> Consider the following example:
>
> struct X
> {
> ~X() {}
> };
>
> template<class T>
> void destroy(T&x)
> { x.~T(); }
>
> int main()
> {
> X *p = (X *)operator new(sizeof(X));
> destroy(*p);
> destroy(*p); // well-defined or undefined?
> operator delete(p);
> }
>
> According to C++11 - 3.8/1, non-trivial destruction ends the life-time
> of an object. Can we assume that a new object of the same type exists
> at the same location immediately after such non-trivial destruction
> has done if its initialization is trivial?

I find the current wording state hard to interpret, but if we consider
the current wording state of

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1116

as representing the committees intention I would say that without
intervening copy of another X object into the storage pointed to by
pointer p, the life-time of the object has not started again, therefore
the second destruction would invoke undefined behaviour. But this also
would mean that the first destruction was invalid, because not object
representation of any X object had ever been copied into the originally
allocated memory.

HTH & Greetings from Bremen,

Daniel Krügler

Johannes Schaub

unread,
May 10, 2012, 7:18:18 PM5/10/12
to
Am 10.05.2012 20:48, schrieb Nikolay Ivchenkov:
> Consider the following example:
>
> struct X
> {
> ~X() {}
> };
>
> template<class T>
> void destroy(T&x)
> { x.~T(); }
>
> int main()
> {
> X *p = (X *)operator new(sizeof(X));
> destroy(*p);
> destroy(*p); // well-defined or undefined?
> operator delete(p);
> }
>
> According to C++11 - 3.8/1, non-trivial destruction ends the life-time
> of an object. Can we assume that a new object of the same type exists
> at the same location immediately after such non-trivial destruction
> has done if its initialization is trivial?
>

According to 3.8/1, there exist all objects of sizeof(X) with suitable
alignment at that object that have trivial initialization :)

Of course that's not how it really is, so 3.8/1 is broken as it is. We
cannot derive a meaningful statement. So let me allow a more elaborative
explanation of my view of your code.

Issue 1116 tries to solve this, so that in your code, also the first
"destroy" is undefined behavior because you have not yet copied another
T object into "*p". Unfortunately this isn't too easy to do with
classes, because a simple "*p = X();" will invoke a member function on
"*p" before even having the "X" object created at "*p". I guess you
would have to memcpy the "X" into "p", something like (i hope I have the
parameter order right).

memcpy(p, &(X const&)X(), sizeof *p);


I don't think that is acceptable for most users, though.

I lately came to the following conclusions (they are by no means
"normative". All of this is by the necessity of the spec being way too
unspecific IMO):

- The start of lifetime of an object of trivial initialization is the
same as the start of existence of that object (it may be "out of
lifetime". It is during its ctor run, and before it. In the latter case,
it's almost unusable except for the non-value uses).

- The start of lifetime of other objects equals the start of lifetime of
them. The existence of the object is implied by its start of lifetime.
It's the "created by the implementation when needed" case of 1.8p1,
despite the "weird" cross reference :)

- The end of lifetime of a class object with a non-trivial dtor may be
different from the end of its existence (in particular, during the dtor
run, the object is out of lifetime but still existent).

- For other objects, the end of lifetime means the stop of existence of
the object, except for objects that were created by a definition,
new-expression or as a temporary (cases where "storage is allocated for
an object of type T"). These objects remain existent but out-of-lifetime.

- Reusing the storage of any object stops the lifetime of the object and
may cause the end of its existence according to the rules above.

Also:

- Certain rules in C++ that refer to an object's existence seem to be
better interpretable when understood to refer to an object's alive
state. For example, 3.8p8.

- A member access expression denotes an "access" by an lvalue of the
object expression too, aswell as by an lvalue of the member. If we
do a write access by an lvalue of type X, we start lifetime of an object
of type X if X has trivial initialization.

So I think the following has undefined behavior since we have an
aliasing violation

struct A { int a; };
struct B { int b; };

A *a = (A*) malloc(sizeof *a);
a->a = 10;
// now an "A" and an "int" object are alive

B *b = (B*)a;
b->a = 0;
// the now a "B" and an "int" object are alive
// (we reused storage)

int x = a->a;
// alias violation: Access by lvalue of type "A"
// to object of type "B".


To come to your code, I think it has undefined behavior, because it
calls a member function (destructor) on an object expression of type
"X", but there is no object of type "X" in existence (see 3.8p5).

You can create one by writing into a data member of "X" or by doing a
"placement-new" of X into that location. The write of the data member
starts the lifetime of the "X" object, which in turn allows the access
of the non-static data member.

Joshua Maurice

unread,
May 10, 2012, 7:23:06 PM5/10/12
to
On May 10, 2:30 pm, Daniel Krügler <daniel.krueg...@googlemail.com>
wrote:
> Am 10.05.2012 20:48, schrieb Nikolay Ivchenkov:
>
> > Consider the following example:
>
> > struct X
> > {
> > ~X() {}
> > };
>
> > template<class T>
> > void destroy(T&x)
> > { x.~T(); }
>
> > int main()
> > {
> > X *p = (X *)operator new(sizeof(X));
> > destroy(*p);
> > destroy(*p); // well-defined or undefined?
> > operator delete(p);
> > }
>
> > According to C++11 - 3.8/1, non-trivial destruction ends the life-time
> > of an object. Can we assume that a new object of the same type exists
> > at the same location immediately after such non-trivial destruction
> > has done if its initialization is trivial?

You saw my other posting just today/yesterday, didn't you?
:)

> I find the current wording state hard to interpret, but if we consider
> the current wording state of
>
> http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1116
>
> as representing the committees intention I would say that without
> intervening copy of another X object into the storage pointed to by
> pointer p, the life-time of the object has not started again, therefore
> the second destruction would invoke undefined behaviour. But this also
> would mean that the first destruction was invalid, because not object
> representation of any X object had ever been copied into the originally
> allocated memory.

I have questions.

Consider:

Example 1:
/* C code */
#include <stdlib.h>
typedef struct Foo { int x; int y; } Foo;
int main(void)
{
Foo * f = malloc(sizeof(Foo));
f -> x = 1;
return f -> x;
}

Example 2:
// Example 1 converted to C++
#include <stdlib.h>
typedef struct Foo { int x; int y; } Foo;
int main(void)
{
Foo * f = (Foo*)malloc(sizeof(Foo));
f -> x = 1;
return f -> x;
}

Example 3:
// C++ code
#include <stdlib.h>
class Foo { public: Foo():x(0),y(0){} int x; int y; };
int main(void)
{
Foo * f = (Foo*)malloc(sizeof(Foo));
f -> x = 1;
return f -> x;
}

Example 1 better be UB-free. It's common C code which is well
understood to be UB-free.

Example 2 better be UB-free. It's similarly well understood that this
common C idiom will work when translated as shown into C++.

Example 3 better have UB. We want this to have UB to permit
optimizations by the compiler. There is no constructor call too
Foo::Foo on that memory, hence no Foo object exists in that memory,
hence UB when you "access" that memory through a Foo lvalue.

Let's go back to your original quote and see how it breaks down:

> without
> intervening copy of another X object into the storage pointed to by
> pointer p, the life-time of the object has not started again,

The relevant code from example 2 and example 3 is:
Foo * f = (Foo*)malloc(sizeof(Foo));
f -> x = 1;
I argue this is (or ought to be) definitionally equivalent to:
Foo * f = (Foo*)malloc(sizeof(Foo));
int * tmp = &((*f).x);
*tmp = 1;

By "your" reasoning, the above code either:
- has an access to a region of memory through a Foo lvalue without a
previous ctor call nor a "copy of another [Foo] object into the
storage", and thus UB, or
- does not have an access to a region of memory through a Foo lvalue,
and thus the only accesses are through int lvalues (specifically the
first access is a "copy of another [int] object into the storage"),
and thus UB-free.

Whether Foo has trivial initialization doesn't come into the analysis
under your rules. Thus, either both 2 and 3 are UB-free, or both 2 and
3 have UB. Neither is acceptable. Thus your rules must be broken.

I have a couple ideas on the intent of the rules, and what they should
be.

For starters, I argue that:
f -> x = 1;
is definitionally equivalent to:
int * tmp = &((*f).x);
*tmp = 1;

Next I think we need to recognize that all writes and reads are done
through:
- primitive lvalues,
- virtual function calls,
- ctors and dtors,
- magic standard library calls, such as std::memset,
(I forget if there's anything else)
Specifically,
int * tmp = &((*f).x);
*tmp = 1;
The expression "&((*f).x);" is neither a read nor a write of the
memory pointed-to by f. It is a read of the pointer value f, an
addition of a compile-time offset, and a write into the stack variable
tmp. No read nor write has happened to *f. In fact, this is a very
important guarantee that we need while writing threading code.

The question is whether we want to call something an "access" which is
neither a read nor a write. I'm partial to saying
int * tmp = &((*f).x);
is an access through a Foo lvalue, and building up the rules from
there. More specifically, I think the rules need to involve escape
analysis on the pointers - the source of the pointer values - in
determining whether the pointer value is valid for use. IIRC, I think
the C committee is leaning in this direction.

As another way to look at the problem, consider the C code:
#include <stdlib.h>
typedef struct T1 { int x; int y; } T1;
typedef struct T2 { int x; int y; } T2;
int main(void)
{
void* p = malloc(sizeof(T1) + sizeof(T2));
T1 * t1 = p;
t1->x = 1;
t1->y = 2;
T2 * t2 = p;
return t2->y;
}
(Un)fortunately, there's a nasty little obscure rule in both C and C++
that allows you to read the common leading members of two (POD)
structs (or something like that). Thus, that example is actually UB-
free AFAIK.

This plays havoc with the analysis. Specifically, consider a slightly
different program:
/* C code */
#include <stdlib.h>
typedef struct T1 { int x; int y; } T1;
typedef struct T2 { int x; int y; } T2;
int main(void)
{
void* p = malloc(sizeof(T1) + sizeof(T2));
T1 * t1 = p;
T2 * t2 = p;
t1->x = 1;
t2->y = 2;
return t1->x;
}
Is that UB? I don't even know. It gets harder as we go on.

This is definitely a C problem which was inherited by C++. With non-C
types, specifically the ones with non-trivial constructors and non-
trivial destructors, I think the rules are far clearer.

Joshua Maurice

unread,
May 11, 2012, 12:22:52 AM5/11/12
to
On May 10, 4:18 pm, Johannes Schaub <schaub.johan...@googlemail.com>
wrote:
>
> - A member access expression denotes an "access" by an lvalue of the
> object expression too, aswell as by an lvalue of the member. If we
> do a write access by an lvalue of type X, we start lifetime of an
> object of type X if X has trivial initialization.
>
> So I think the following has undefined behavior since we have an
> aliasing violation
>
> struct A { int a; };
> struct B { int b; };
>
> A *a = (A*) malloc(sizeof *a);
> a->a = 10;
> // now an "A" and an "int" object are alive
>
> B *b = (B*)a;
> b->a = 0;
> // the now a "B" and an "int" object are alive
> // (we reused storage)
>
> int x = a->a;
> // alias violation: Access by lvalue of type "A"
> // to object of type "B".

I agree. Something like this seems to be the sensible approach for
both C, and C++ for backwards compatibility with C (and supporting a
lot of existing C++ code).

> To come to your code, I think it has undefined behavior, because it
> calls a member function (destructor) on an object expression of type
> "X", but there is no object of type "X" in existence (see 3.8p5).
>
> You can create one by writing into a data member of "X" or by doing
> a "placement-new" of X into that location. The write of the data
> member starts the lifetime of the "X" object, which in turn allows
> the access of the non-static data member.

This seems like a reasonable suggestion. I'd have to sit and think on
it.

I agree with an earlier part of your post (snipped) that users
probably want to be able to call member operator= as the first action
on a piece of memory for a POD (or "simple") struct, but this seems to
be disallowed by your suggested rules. PS: If we go to allow such a
thing for POD objects, then the existing restriction of "reading
uninitialized (primitive) objects is UB" is probably sufficient.

Francis Glassborow

unread,
May 11, 2012, 6:20:55 AM5/11/12
to
On 11/05/2012 00:23, Joshua Maurice wrote:
>
> This plays havoc with the analysis. Specifically, consider a slightly
> different program:
> /* C code */
> #include<stdlib.h>
> typedef struct T1 { int x; int y; } T1;
> typedef struct T2 { int x; int y; } T2;
> int main(void)
> {
> void* p = malloc(sizeof(T1) + sizeof(T2));
> T1 * t1 = p;
> T2 * t2 = p;
> t1->x = 1;
> t2->y = 2;
> return t1->x;
> }
> Is that UB? I don't even know. It gets harder as we go on.
>
> This is definitely a C problem which was inherited by C++. With non-C
> types, specifically the ones with non-trivial constructors and non-
> trivial destructors, I think the rules are far clearer.
>
>

I agree that the status of such code is difficult to determine, C had
good reasons (well at least reasons) for its rule about access to a
common set of members. At least in part that goes back to C's need for
compatible types because struct type names do not have static linkage.
By contrast C++ struct/class names do have static linkage and that means
that reuse of a name with a different definition in another compilation
unit is ill-formed but no diagnostic is required.


C++ does not have the concept of compatible types. Even if some reading
of the c++ Standard makes the above defined behaviour I am 100% certain
that that should not be the case.

However regardless of the actual requirements of the Standard, any
programmer who writes code relying on the above having well-defined
behaviour is dangerously incompetent, not least because it would produce
a maintenance nightmare (which the C concept of compatible types already
creates in C)

Francis

Nikolay Ivchenkov

unread,
May 11, 2012, 6:39:07 AM5/11/12
to
On 11 May, 01:30, Daniel Krügler <daniel.krueg...@googlemail.com>
wrote:
> Am 10.05.2012 20:48, schrieb Nikolay Ivchenkov:
> > struct X
> > {
> > ~X() {}
> > };
>
> ....
>
> I find the current wording state hard to interpret, but if we consider
> the current wording state of
>
> http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1116
>
> as representing the committees intention I would say that without
> intervening copy of another X object into the storage pointed to by
> pointer p, the life-time of the object has not started again

Why not? My X is not trivially copyable.

On 11 May, 03:18, Johannes Schaub <schaub.johan...@googlemail.com>
wrote:
>
> Issue 1116 tries to solve this, so that in your code, also the first
> "destroy" is undefined behavior because you have not yet copied another
> T object into "*p".

AFAICS, the proposed resolution of issue 1116 is not relevant here. A
trivially copyable class cannot have non-trivial destructor.

> You can create one by writing into a data member of "X" or by doing a
> "placement-new" of X into that location.

I don't want to do such things. I'm implementing my own Vector, it is
able to create default-initialized elements:

// creates n default-initialized objects of type X
Vector<X> v(n, DefaultInitialize());

I want to optimize creation of sequence of objects with trivial
initialization: the complexity of such operation should be constant
(finite sequence of no-ops = no-op) if this is possible within the
current C++ object model.

Johannes Schaub

unread,
May 11, 2012, 5:20:33 PM5/11/12
to
Am 11.05.2012 12:39, schrieb Nikolay Ivchenkov:
> On 11 May, 01:30, Daniel Krügler<daniel.krueg...@googlemail.com>
> wrote:
>> Am 10.05.2012 20:48, schrieb Nikolay Ivchenkov:
>>> struct X
>>> {
>>> ~X() {}
>>> };
>>
>> ....
>>

> On 11 May, 03:18, Johannes Schaub<schaub.johan...@googlemail.com>
> wrote:
>>
>> Issue 1116 tries to solve this, so that in your code, also the
>> first "destroy" is undefined behavior because you have not yet
>> copied another T object into "*p".
>
> AFAICS, the proposed resolution of issue 1116 is not relevant
> here. A trivially copyable class cannot have non-trivial destructor.
>

I'm sorry. I wasn't aware that the destructor needs to be trivial for
a class to be trivially copyable.

I think it is strange that neither the first bullet of 1116 nor the
second bullet appears to match for your case.

>> You can create one by writing into a data member of "X" or by doing
>> a "placement-new" of X into that location.
> I don't want to do such things. I'm implementing my own Vector, it
> is able to create default-initialized elements:
>
> // creates n default-initialized objects of type X
> Vector<X> v(n, DefaultInitialize());
>
> I want to optimize creation of sequence of objects with trivial
> initialization: the complexity of such operation should be constant
> (finite sequence of no-ops = no-op) if this is possible within the
> current C++ object model.

An object with trivial initialization (like "int" or "X") is not
automatically created just because you have memory and suitable
alignment. But since you provide a value initialized source value and
presumably have to initialize the memory with that value using
placement new anyway, that would look considerably different from your
testcase that you showed.

What am I missing here?

Johannes Schaub

unread,
May 11, 2012, 5:23:04 PM5/11/12
to
Am 11.05.2012 01:18, schrieb Johannes Schaub:
> Am 10.05.2012 20:48, schrieb Nikolay Ivchenkov:
>> Consider the following example:
>>
>> struct X
>> {
>> ~X() {}
>> };
>>
>> template<class T>
>> void destroy(T&x)
>> { x.~T(); }
>>
>> int main()
>> {
>> X *p = (X *)operator new(sizeof(X));
>> destroy(*p);
>> destroy(*p); // well-defined or undefined?
>> operator delete(p);
>> }
>>
>> According to C++11 - 3.8/1, non-trivial destruction ends the
>> life-time of an object. Can we assume that a new object of the same
>> type exists at the same location immediately after such non-trivial
>> destruction has done if its initialization is trivial?
>>
>
> I lately came to the following conclusions (they are by no means
> "normative". All of this is by the necessity of the spec being way
> too unspecific IMO):
>
> - The start of lifetime of an object of trivial initialization is
> the same as the start of existence of that object (it may be "out of
> lifetime". It is during its ctor run, and before it. In the latter
> case, it's almost unusable except for the non-value uses).
>
> - The start of lifetime of other objects equals the start of
> lifetime of them. The existence of the object is implied by its
> start of lifetime. It's the "created by the implementation when
> needed" case of 1.8p1, despite the "weird" cross reference :)
>

I am sorry for not being awake when I wrote that text. What I wanted
to say in those two bullets:

- The start of lifetime of an object with trivial initialization is
the same as the start of existence of that object. If the object has
no explicit begin of existence (by a definition, new-expression or
temporary expression like "string()"), the existence of the object is
implied by the start of lifetime ("created by the implementation when
needed", 1.8p1).

- The start of lifetime of other objects may happen after the object
started existence (applies to class types during their ctor. lifetime
starts when the ctor finishes execution successfully).

I hope I could clarify my view on it! See my other post for examples
of "implied start of existence". I think one of them is doing a write
access by an lvalue of type "X", starting existence of an object of
type X if X has a trivial default constructor and there is no object
already existent of type "X" at its location (diregarding
cv-qualifiers) (example: doing a write with an lvalue of type
"volatile int" to an object of type "int" will not create an object of
type "volatile int" / will not destroy the "int" object).

Joshua Maurice

unread,
May 11, 2012, 5:30:13 PM5/11/12
to
On May 11, 3:20 am, Francis Glassborow
<francis.glassbo...@btinternet.com> wrote:
> I agree that the status of such code is difficult to determine, C had
> good reasons (well at least reasons) for its rule about access to a
> common set of members. At least in part that goes back to C's need for
> compatible types because struct type names do not have static linkage.
> By contrast C++ struct/class names do have static linkage and that means
> that reuse of a name with a different definition in another compilation
> unit is ill-formed but no diagnostic is required.

I thought the rule to allow reading and writing of the common leading
members was done mostly to implement a "poor man's" inheritance. This
is not true?

Also, what do you mean by "static linkage"? Checking my random draft
copy of the C standard and C++03, I see that the linkage types in both
C and C++ are "external", "internal", and "none". In C, "6.2.2
Linkages of identifiers / 6" says that struct tag identifiers have no
linkage. In C++03, "3.5 Program and linkage / 4" says: "A name having
namespace scope has external linkage if it is the name of [...] a
named class (clause 9)". Offhand, if I were to hear "static linkage",
I would assume one of "internal" or "none". However, what you said
only makes sense if you meant "external linkage" when you said "static
linkage". Right?

Francis Glassborow

unread,
May 12, 2012, 6:08:00 AM5/12/12
to
Yes, sorry, I was tired at the time and did not recall the correct
term.

Yes, C's leading member rule is to support 'poor man's inheritance'
but the basic problem is that a struct name in C has internal linkage
and so they had to introduce the concept of compatible types to allow
a type to be used in more than one TU. I sometimes wish that the C
enthusiasts would recognise how badly compromised their type system
is. This is a serious problem when we try to maintain compatibility
between C and C++. Look at some of the contortions needed to support
complex. In C it has to be a fundamental type, C++ not only has no
need for that but is better (IMO) by having it as a library type.

Joshua Maurice

unread,
May 15, 2012, 1:20:01 PM5/15/12
to
On May 12, 3:08 am, Francis Glassborow
Indeed. I often wonder why they didn't deprecate and remove a lot of
things, such as the K&R implicit function declaration rule. That is
just asking for pain.

Oh, and that's why C has this concept of "compatible type" instead of
a C++like One Definition Rule? Interesting. Thanks. Good to know. That
is pretty broken. I mean, I think "ODR violations lead to UB" is
broken too, but at least that's an implementation constraint, not some
hokey rule that (maybe) allows some particularly twisted results, like
some of examples in this thread.

One of these days, I'll try to reread the relevant sections of the C
standard and try to make some sense of the "effective type" rules. I
think that's the name of the rules that describe how you can write
into malloc'd memory through lvalues obtained by using a struct lvalue
and the member (dot) operator. I didn't get very far in my
comprehension the first time. Maybe this time, eh? ;)
0 new messages