std::construct_at and object lifetime?
Asked Answered
S

2

6

I was reviewing a code I proposed to initialize a std::array at compile-time for non-default-constructible objects: https://mcmap.net/q/905632/-idiom-for-initializing-an-std-array-using-a-generator-function-taking-the-index

in the second version I'm doing:

// creating storage on std::byte to benefit from "implicit object creation"
alignas(alignof(T)) std::byte storage[sizeof(std::array<T, N>)];
std::array<T, N>* const Array =
    std::launder(reinterpret_cast<std::array<T, N>*>(storage));

// initializing objects in the storage
T* it =
    Array->data();  // construct objects in storage through std::construct_at
for (std::size_t i = 0; i != N; ++i) {
    std::construct_at(it, gen(static_cast<int>(i))); // UB?
    // new (it) T(gen(static_cast<int>(i))); // not UB?
    ++it;
}

LIVE

I wanted to rely on the fact that std::array is an implicit lifetime type but I'm thinking now that I'm UB for the following reason: Array has indeed started its lifetime but none of his subobjects of type T:

Some operations are described as implicitly creating objects within a specified region of storage.[...] [ Note: Such operations do not start the lifetimes of subobjects of such objects that are not themselves of implicit-lifetime types. — end note ]

https://timsong-cpp.github.io/cppwp/n4861/intro.object#10

Does std::construct_at starts this lifetime or just calls the constructor of an object that is already alived inplace (in which case my code would be UB)?

What made me have doubts is the example provided in cppreference that goes through a std::bit_cast to start an object lifetime (thus, AFAIU, in a different memory location than the provided storage).


further research: std::construct_at:

Effects: Equivalent to: return ::new (voidify(*location)) T(std::forward(args)...);

https://timsong-cpp.github.io/cppwp/n4861/specialized.construct#2

Objects creation 'emphasis mine':

An object is created by a definition, by a new-expression, by an operation that implicitly creates objects (see below), when implicitly changing the active member of a union, or when a temporary object is created.

https://timsong-cpp.github.io/cppwp/n4861/intro.object#1

Linking these two section, is it correct to say that std::construct_at, in fact, actually start an object lifetime at the given location?

Spoke answered 8/10 at 9:48 Comment(18)
"Array has indeed started its lifetime but none of his subobjects of type T" - This seems wrong to me, and not what implicit lifetime is meant to permit. If it does, I'll be surprised.Firenew
@user:817643 what part seems wrong to you? The fact that the subobjects didn't started their lifetime?Spoke
An array is little more than a bunch of objects stored consecutively. The lifetime of the array is the same as the one of its objects. Simply creating an array by laundering it in a byte storage does not initialize anything and using any object before initialization would be UB. But as you correctly initialize them with construct_at, no UB is involved here.Hannahhannan
@SergeBallesta I don't pretend the array to be initialized, only that its lifetime started (implicit lifetime type). At first I thought as you are that the lifetime of the array was the same of the object it contains but I have some doubts now (see for instance https://mcmap.net/q/1780378/-how-to-create-an-array-and-start-its-lifetime-without-starting-the-lifetime-of-any-of-its-elements/21691539)Spoke
@Spoke - Yes, that exactly. It's possible your code is well-formed, due to implicit life rules (I confess to not know for sure). But allowing half baked objects to exist at all is a serious paradigm shift that I don't think is intended.Firenew
@user:817643 Thx. There is perhaps too much of context in my question. The lifetime issue is indeed open IMHO, but the specific topic here is does std::construct_at start object lifetime?Spoke
@StoryTeller-UnslanderMonica: That is exactly what placement new was made for when using dynamic memory. If that two-time construction of object was not allowed, what could be the use case for construct_at? But using it requires manual destruction of the objects with destruct_at if their destructor is expected to have any effect (including mere destruction of subobjects).Hannahhannan
@StoryTeller-UnslanderMonica: But I have to confess that I am not sure at all of the real moment at which the life time of the objects starts and ends...Hannahhannan
@SergeBallesta - Placement new created objects, but it didn't at all circumvent the "vector problem" that implicit-lifetimes are meant to address so that you can even use placement new. All of that comment is beside the point.Firenew
@Oersted: Is your question about what a decent compiler will accept, or about what a strict reading of the standard allows? For the latter case, adding the language-lawyer tag could help...Hannahhannan
timsong-cpp.github.io/cppwp/n4861/intro.object#10: AFAIU, subobjects lifetime is not started if they are not themselves of implicit lifetime types.Spoke
OT: alignas(alignof(T)) can be shortened to alignas(T).Sarnoff
@Eljay there are C++ objects and there are Object oriented objects. They are not the same.Presidency
@DanielLangr, Pepijn Kramer • I was mistaken. I confused the inability to return an array with an array not being an object. At some point in time (decades ago) my brain associated arrays as "not an object" because of that. mea culpa, and thank you for the correction.Lemke
You should be constructing the std::array object inside the storage before you can then safely access its members, like data(). The code shown is merely type-casting the storage without actually creating the std::array object. std::array is itself a struct after all. It needs to be constructed and destroyed like any other class type. Especially if T is not a trivial type.Glorify
@RemyLebeau: It's an implicit lifetime type; it's already there.Attah
@NicolBolas are you sure? Is std::array covered by the rules outlined in en.cppreference.com/w/cpp/named_req/ImplicitLifetimeType ? Again, I go back to the issue with std::array holding a non-trivial type, does that allow or disallow std::array from being an implicit lifetime type?Glorify
@RemyLebeau Yes, std::array<T,N> is an implicit-lifetime type due to class.prop/9, independently of what T is.Sarnoff
K
3

According to [basic.life]/1, an object's lifetime starts immediately after its initialization is complete, including "vacuous initialization", which is the case where the initialization phase does nothing at all (such as default-initialization for an object of scalar type).

std::construct_at is specified to use the new operator, and new is specified to initialize the object created ([expr.new]/24), so the lifetime begins. Note that the new operator, similarly to an object definition, performs "ordinary" object creation that, unlike implicit object creation, is guaranteed to recursively initialize bases and members according to the usual rules (e.g., [class.base.init], [dcl.init.aggr]).

Kumamoto answered 8/10 at 23:31 Comment(3)
"new operator` → "new expression". Please, do not confuse these two terms; new operator (operator new) is an allocation function.Sarnoff
@DanielLangr Operators are used to build up complex expressions from simpler ones. For example, a conditional expression x ? y : z is an expression that's built up by applying the conditional operator ?: to its three operands. By analogy, the keyword new that introduces a new-expression is also an operator. Unfortunately as you point out this is one of the cases where the operator (syntactic construct) doesn't do exactly what the corresponding operator function does.Kumamoto
You're right, the new expression is also listed here: en.cppreference.com/w/cpp/language/operator_precedence. Even a function call is operator according to this page. Thanks for clarification.Sarnoff
A
1

Really, the only thing more that could be linked to to show you that you are correct is [expr.new]/1:

The new-expression attempts to create an object of the type-id or new-type-id to which it is applied.

So yes, std::construct_at creates objects. If it required the object to exist, that would be stated in its requirements. And it'd be very weird, since calling a constructor on an existing object... isn't really a thing.

Attah answered 8/10 at 18:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.