Can an object be nested within another of the same type?

188 views
Skip to first unread message

Chris Hallock

unread,
Sep 11, 2017, 11:58:15 PM9/11/17
to ISO C++ Standard - Discussion
For example, does the new-expression in the program below
1) end the lifetime of x (by reusing its storage) and create another object in its place, or
2) create a new A object nested within x?

#include <new>

struct A { unsigned char buf[1]; };
static_assert(sizeof(A) == 1); // A can fit within A::buf

int main()
{
    A x
{};
   
new (x.buf) A{};
}

Thiago Macieira

unread,
Sep 12, 2017, 8:47:01 AM9/12/17
to std-dis...@isocpp.org
On Monday, 11 September 2017 20:58:15 PDT Chris Hallock wrote:
> For example, does the *new-expression* in the program below
> 1) end the lifetime of x (by reusing its storage) and create another object
> in its place, or
> 2) create a new A object nested within x?

It's not nested, it's actually the same object. The only way an object is big
enough to contain an object of the same size is if that sub-object occupies
the entire object.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center

Richard Smith

unread,
Sep 12, 2017, 12:55:04 PM9/12/17
to std-dis...@isocpp.org
On 11 September 2017 at 20:58, Chris Hallock <christoph...@gmail.com> wrote:
For example, does the new-expression in the program below
1) end the lifetime of x (by reusing its storage) and create another object in its place, or
2) create a new A object nested within x?

Under [intro.object]p3, the original object provides storage for the new object. Under [basic.life]p8, the name of the original object is rebound to the new object. But we did not meet the requirements of [basic.life]p5, so the original object's lifetime does not end. I think you end up with a "dangling" outer A object that can no longer be named, providing storage for an inner A object that is now named x.

In terms of the observable behavior of the program, I think this is equivalent to replacing the original object with a new object.
 
#include <new>

struct A { unsigned char buf[1]; };
static_assert(sizeof(A) == 1); // A can fit within A::buf

int main()
{
    A x
{};
   
new (x.buf) A{};
}

--

---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+unsubscribe@isocpp.org.
To post to this group, send email to std-dis...@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.

Message has been deleted

Chris Hallock

unread,
Sep 12, 2017, 2:00:49 PM9/12/17
to ISO C++ Standard - Discussion
On Tuesday, September 12, 2017 at 12:55:04 PM UTC-4, Richard Smith wrote:
On 11 September 2017 at 20:58, Chris Hallock <christoph...@gmail.com> wrote:
For example, does the new-expression in the program below
1) end the lifetime of x (by reusing its storage) and create another object in its place, or
2) create a new A object nested within x?

Under [intro.object]p3, the original object provides storage for the new object. Under [basic.life]p8, the name of the original object is rebound to the new object. But we did not meet the requirements of [basic.life]p5, so the original object's lifetime does not end. I think you end up with a "dangling" outer A object that can no longer be named, providing storage for an inner A object that is now named x.

In terms of the observable behavior of the program, I think this is equivalent to replacing the original object with a new object.


(Messed up my previous reply, trying again...)

But if the previous object didn't end, then rebinding doesn't happen — [basic.life]/8 starts "If, after the lifetime of an object has ended...". What's unclear is whether the new-expression constitutes a re-use of x's storage or of x.buf's storage.

If the new-expression were new (&x) A{} instead of new (x.buf) A{}, does that change anything? I can't find any Standardese that clearly differentiates the behavior of these two expressions.

If nesting does indeed occur, that would seem to violate the assumed principle of the C++ object model that no two objects of the same type can exist simultaneously at the same address (besides unsigned char or std::byte, maybe). std::launder() in particular appears to make that assumption.

Richard Smith

unread,
Sep 12, 2017, 3:17:21 PM9/12/17
to std-dis...@isocpp.org
On 12 September 2017 at 11:00, Chris Hallock <christoph...@gmail.com> wrote:
On Tuesday, September 12, 2017 at 12:55:04 PM UTC-4, Richard Smith wrote:
On 11 September 2017 at 20:58, Chris Hallock <christoph...@gmail.com> wrote:
For example, does the new-expression in the program below
1) end the lifetime of x (by reusing its storage) and create another object in its place, or
2) create a new A object nested within x?

Under [intro.object]p3, the original object provides storage for the new object. Under [basic.life]p8, the name of the original object is rebound to the new object. But we did not meet the requirements of [basic.life]p5, so the original object's lifetime does not end. I think you end up with a "dangling" outer A object that can no longer be named, providing storage for an inner A object that is now named x.

In terms of the observable behavior of the program, I think this is equivalent to replacing the original object with a new object.


(Messed up my previous reply, trying again...)

But if the previous object didn't end, then rebinding doesn't happen — [basic.life]/8 starts "If, after the lifetime of an object has ended...".

Good point. I think most of the above still applies: the lifetime rule in [basic.life]/1.4 requires that we first consider whether [intro.object]/3 applies. It does, and the buffer provides storage for the new object, so the lifetime of 'x' does not end. So 'x' is not rebound to the new object, and its buffer holds another X object.
 
What's unclear is whether the new-expression constitutes a re-use of x's storage or of x.buf's storage.

If the new-expression were new (&x) A{} instead of new (x.buf) A{}, does that change anything? I can't find any Standardese that clearly differentiates the behavior of these two expressions.

I agree. The wording in [intro.object] and [basic.life] concern themselves with the storage address represented by a pointer, not the object to which it points.

If nesting does indeed occur, that would seem to violate the assumed principle of the C++ object model that no two objects of the same type can exist simultaneously at the same address (besides unsigned char or std::byte, maybe).

That holds even for unsigned char / std::byte: if you placement new, say, a "struct A { unsigned char c; };" into an "unsigned char buffer[1];", the lifetime of the unsigned char object buffer[0] ends even though the lifetime of the buffer array itself does not.

Perhaps Thiago's response is best: [intro.object]p3 should not apply if the array is nested within an object of the new object's type. The containing object must then necessarily be within its lifetime, have exactly overlapping storage, and contain no const or reference members, implying we instead reach either [intro.object]p2 (if that object is a subobject) or [basic.life]p8 (otherwise).

Would you like to file this as a defect report?
 
std::launder() in particular appears to make that assumption.

--
Message has been deleted
Message has been deleted

Chris Hallock

unread,
Sep 13, 2017, 12:14:56 PM9/13/17
to ISO C++ Standard - Discussion
(Google Groups must be hungry, because it keeps eating my posts...)


On Tuesday, September 12, 2017 at 3:17:21 PM UTC-4, Richard Smith wrote:
On 12 September 2017 at 11:00, Chris Hallock <christoph...@gmail.com> wrote:
On Tuesday, September 12, 2017 at 12:55:04 PM UTC-4, Richard Smith wrote:
On 11 September 2017 at 20:58, Chris Hallock <christoph...@gmail.com> wrote:
For example, does the new-expression in the program below
1) end the lifetime of x (by reusing its storage) and create another object in its place, or
2) create a new A object nested within x?

Under [intro.object]p3, the original object provides storage for the new object. Under [basic.life]p8, the name of the original object is rebound to the new object. But we did not meet the requirements of [basic.life]p5, so the original object's lifetime does not end. I think you end up with a "dangling" outer A object that can no longer be named, providing storage for an inner A object that is now named x.

In terms of the observable behavior of the program, I think this is equivalent to replacing the original object with a new object.


(Messed up my previous reply, trying again...)

But if the previous object didn't end, then rebinding doesn't happen — [basic.life]/8 starts "If, after the lifetime of an object has ended...".

Good point. I think most of the above still applies: the lifetime rule in [basic.life]/1.4 requires that we first consider whether [intro.object]/3 applies. It does, and the buffer provides storage for the new object, so the lifetime of 'x' does not end. So 'x' is not rebound to the new object, and its buffer holds another X object.
 
What's unclear is whether the new-expression constitutes a re-use of x's storage or of x.buf's storage.

If the new-expression were new (&x) A{} instead of new (x.buf) A{}, does that change anything? I can't find any Standardese that clearly differentiates the behavior of these two expressions.

I agree. The wording in [intro.object] and [basic.life] concern themselves with the storage address represented by a pointer, not the object to which it points.

If nesting does indeed occur, that would seem to violate the assumed principle of the C++ object model that no two objects of the same type can exist simultaneously at the same address (besides unsigned char or std::byte, maybe).

That holds even for unsigned char / std::byte: if you placement new, say, a "struct A { unsigned char c; };" into an "unsigned char buffer[1];", the lifetime of the unsigned char object buffer[0] ends even though the lifetime of the buffer array itself does not.

Perhaps Thiago's response is best: [intro.object]p3 should not apply if the array is nested within an object of the new object's type. The containing object must then necessarily be within its lifetime, have exactly overlapping storage, and contain no const or reference members, implying we instead reach either [intro.object]p2 (if that object is a subobject) or [basic.life]p8 (otherwise).

I agree that this should be prohibited. Would it suffice to add another constraint to [intro.object]/3? Something like "the created object would have an address different from any existing object of the same type (ignoring cv qualification)". Then new (x.buf) A{} would replace x instead of nest within, because we prohibited nesting.


Would you like to file this as a defect report?

I'd be happy to submit an issue report, if you're inviting me to (?).

Richard Smith

unread,
Sep 13, 2017, 3:35:27 PM9/13/17
to std-dis...@isocpp.org
I think it'd be better to say something like "the array is not nested within an object of the same type (ignoring cv qualification) that is within its lifetime", because I think your rule creates a circularity: if the A object is nested within the array, then it violates that rule and is not nested within. Otherwise, it ends the lifetime of the enclosing A object and no longer violates that rule, which means it *is* nested within the array and so does not end the lifetime of the enclosing A object.
 
Would you like to file this as a defect report?

I'd be happy to submit an issue report, if you're inviting me to (?).

I am :) 

Chris Hallock

unread,
Sep 13, 2017, 6:46:30 PM9/13/17
to ISO C++ Standard - Discussion
Ah, that makes sense.
 

Would you like to file this as a defect report?

I'd be happy to submit an issue report, if you're inviting me to (?).

I am :) 

I've got that ball rolling. Thanks for your help!

Language Lawyer

unread,
Sep 16, 2018, 2:34:52 PM9/16/18
to ISO C++ Standard - Discussion
On Wednesday, September 13, 2017 at 10:35:27 PM UTC+3, Richard Smith wrote:
On 13 September 2017 at 09:14, Chris Hallock <christoph...@gmail.com> wrote:
(Google Groups must be hungry, because it keeps eating my posts...)

On Tuesday, September 12, 2017 at 3:17:21 PM UTC-4, Richard Smith wrote:
On 12 September 2017 at 11:00, Chris Hallock <christoph...@gmail.com> wrote:
On Tuesday, September 12, 2017 at 12:55:04 PM UTC-4, Richard Smith wrote:
On 11 September 2017 at 20:58, Chris Hallock <christoph...@gmail.com> wrote:
For example, does the new-expression in the program below
1) end the lifetime of x (by reusing its storage) and create another object in its place, or
2) create a new A object nested within x?

Under [intro.object]p3, the original object provides storage for the new object. Under [basic.life]p8, the name of the original object is rebound to the new object. But we did not meet the requirements of [basic.life]p5, so the original object's lifetime does not end. I think you end up with a "dangling" outer A object that can no longer be named, providing storage for an inner A object that is now named x.

In terms of the observable behavior of the program, I think this is equivalent to replacing the original object with a new object.


(Messed up my previous reply, trying again...)

But if the previous object didn't end, then rebinding doesn't happen — [basic.life]/8 starts "If, after the lifetime of an object has ended...".

Good point. I think most of the above still applies: the lifetime rule in [basic.life]/1.4 requires that we first consider whether [intro.object]/3 applies. It does, and the buffer provides storage for the new object, so the lifetime of 'x' does not end. So 'x' is not rebound to the new object, and its buffer holds another X object.
 
What's unclear is whether the new-expression constitutes a re-use of x's storage or of x.buf's storage.

If the new-expression were new (&x) A{} instead of new (x.buf) A{}, does that change anything? I can't find any Standardese that clearly differentiates the behavior of these two expressions.

I agree. The wording in [intro.object] and [basic.life] concern themselves with the storage address represented by a pointer, not the object to which it points.

If nesting does indeed occur, that would seem to violate the assumed principle of the C++ object model that no two objects of the same type can exist simultaneously at the same address (besides unsigned char or std::byte, maybe).

That holds even for unsigned char / std::byte: if you placement new, say, a "struct A { unsigned char c; };" into an "unsigned char buffer[1];", the lifetime of the unsigned char object buffer[0] ends even though the lifetime of the buffer array itself does not.

Perhaps Thiago's response is best: [intro.object]p3 should not apply if the array is nested within an object of the new object's type. The containing object must then necessarily be within its lifetime, have exactly overlapping storage, and contain no const or reference members, implying we instead reach either [intro.object]p2 (if that object is a subobject) or [basic.life]p8 (otherwise).

I agree that this should be prohibited. Would it suffice to add another constraint to [intro.object]/3? Something like "the created object would have an address different from any existing object of the same type (ignoring cv qualification)". Then new (x.buf) A{} would replace x instead of nest within, because we prohibited nesting.

I think it'd be better to say something like "the array is not nested within an object of the same type (ignoring cv qualification) that is within its lifetime"

Your suggestion is also not perfect, because it does not prevent an `unsigned char` or `std::byte` array of size `N` to be nested within the array of the same size and type arbitrary number of times without ending the lifetime of the original array and all the arrays created before.

One thing that could prevent this is a non-negative unspecified array overhead. But, for example, in Itanium C++ ABI the overhead is zero for trivially destructible types (https://itanium-cxx-abi.github.io/cxx-abi/abi.html#array-cookies (it says it is zero "if the array element type T has a trivial destructor (12.4 [class.dtor])" but in fact it is zero for non-class types too)).

Language Lawyer

unread,
Sep 16, 2018, 2:45:24 PM9/16/18
to std-dis...@isocpp.org
On 16/09/18 21:34, Language Lawyer wrote:
> On Wednesday, September 13, 2017 at 10:35:27 PM UTC+3, Richard Smith wrote:
>>
>> On 13 September 2017 at 09:14, Chris Hallock <christoph...@gmail.com
>> <javascript:>> wrote:
>>
>>> (Google Groups must be hungry, because it keeps eating my posts...)
>>>
>>> On Tuesday, September 12, 2017 at 3:17:21 PM UTC-4, Richard Smith wrote:
>>>>
>>>> On 12 September 2017 at 11:00, Chris Hallock <christoph...@gmail.com>
>>>> wrote:
>>>>
>>>>> On Tuesday, September 12, 2017 at 12:55:04 PM UTC-4, Richard Smith
>>>>> wrote:
>>>>>>
>>>>>> On 11 September 2017 at 20:58, Chris Hallock <christoph...@gmail.com>
>>>>>> wrote:
>>>>>>
>>>>>>> For example, does the *new-expression* in the program below
Oops. I was wrong.

You may nest an `unsigned char` or `std::byte` array inside an array of the same type only *once*.
But then the nested array won't provide storage for further arrays of the same size and type because it is nested within an object of the same type.

But still one can haz two objects with the same type and overlapping lifetimes at the same address.
Reply all
Reply to author
Forward
0 new messages