At what point does the lifetime of a trivial type created by placement-new start?
Asked Answered
M

1

8

During a dive into dynamic memory, it occurred to me it appears contradictory how trivial types begin their lifetime. Consider the snippet

void* p = ::operator new(sizeof(int));  // 1
// 2
new (p) int; // 3

When does the int start its lifetime?

  1. Only acquires storage, ::operator new is specified to have the effect (from [new.delete.single])

    The allocation functions called by a new-expression to allocate size bytes of storage. [...] allocates storage suitably aligned to represent any object of that size provided the object's type does not have new-extended alignment.

    Given that acquiring storage is insufficient in creating an object, the int cannot have begin its lifetime here.

  2. At this point, suitbale storage for the int has been acquired.

  3. The int is created by placement new. But somehow its lifetime didn't begin here, since from [basic.life]

    [...] An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its subobjects is initialized by a constructor other than a trivial default constructor. 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-vacuous initialization, its initialization is complete [...]

    int is neither a class nor aggregate type, hence it has vacuous initialization. Therefore only the first bullet applies. However, this is clearly not when storage is obtained and therefore cannot be when its lifetime starts.

Some context

Allocators are required to return memory without constructing its elements. Yet this doesn't make sense with trivial types. The effects of a.allocate(n) with a an allocator object for type T is

Memory is allocated for n objects of type T but objects are not constructed.

Murex answered 28/2, 2018 at 20:46 Comment(5)
related “constructing” a trivially-copyable object with memcpy there are several linked questions that cover several variations of this theme one of them will almost surely cover this case.Scribner
Relevant is P0593 (revision 2 recently updated), although that hasn't gotten so far yet as to propose concrete changes to the standardThoma
this isn't a dupe of the other threads (the placement-new is substantially different to memcpy or whatever) . The original title of this thread was misleading as to the real issue it raisesThoma
@M.M: But the answer is the same. The placement new is what causes the object to be created, pursuant to [intro.object]/1. You cannot start the lifetime of an object that doesn't exist.Mariko
@NicolBolas Nothing in the answer you linked applies to this question. In the code in this question there is an object created by placement-new; that answer (and the question it answers) is talking about the situation where there are no objects createdThoma
T
7

Technically, the new-expression always obtains storage. The code new(p) int both obtains storage and creates an object, and according to [basic.life]/1, the object's lifetime began when new(p) int obtained the storage.

According to N4659 [expr.new], the code new(p) int generates a call to an allocation function ::operator new(sizeof(int), p). And under [new.delete.placement], the standard library defines such a function:

void* operator new(std::size_t size, void* ptr) noexcept;

Returns: ptr.

Remarks: Intentionally performs no other action.

Although "no other action" is performed, and probably the implementation will optimize out any actual function call, this call to an allocation function still counts as obtaining storage for the object being created by the new-expression.

Thoma answered 28/2, 2018 at 21:41 Comment(8)
That's unexpected. I like how the standard knows how this looks totally bogus but is there to make the language consistentMurex
@PasserBy The standard would seem more consistent if [basic.life]/1 were completed by [intro.object]/1 in this answer: [...]An object is created by a definition, by a new-expression, when implicitly changing the active member of a union, or when a temporary object is createdBrinkmanship
@Brinkmanship When is a definition executed?Disgust
@Disgust That is a trap no? I think before C++20 it was when the placement allocation function returns. But now, it is when the vacuous initialization of the int has been completed.Brinkmanship
This is outdated since C++20 is published. See also P0593R6 (adopted 2020-02).Abeabeam
@Abeabeam Is it a moot point? The int object created by new(p) int didn't exist before new(p) int, and does exist after new(p) int. Referring to the exact code in the question here. You could implicitly create an int object in the space before calling new(p) int, but that's a different object than the one new(p) int createsThoma
@Thoma In certain cases the new-expression can be omitted and the effect is same. If there is such an new-expression, it has no effect besides returning a pointer value to the sutiable created object as the result, creating no object (because any object the result points to is considered already created before evaluating the new-expression). This is relavant to the context in the original question, albeit not necessarily about the code in the question.Abeabeam
To be specific, if the storage is provided by the same type of an suitable created object, there is no new object created in place, otherwise the effect of the pseudo destructor call will be unclear about which object to be destroyed. And at least for int, the pointer value of same address should only alias to the same object it pointed to. This is the practical basis of strict aliasing. The "implicitly creating object" wording is not very intuitive, though, but it is still clear about "creates and starts the lifetime of zero or more objects of implicit-lifetime types" (emphasize mine).Abeabeam

© 2022 - 2024 — McMap. All rights reserved.