10

I've always assumed that "creating an object" is the same thing as "starting its lifetime" (and not the same thing as allocating storage for it).

But I've recently been told that "creating an object" means something else, and that starting object's lifetime recursively creates all its subobjects, even it doesn't start their lifetimes, e.g.:

  • When starting the lifetime of a union without setting an active element, all the elements still get created.

  • When the lifetime of an array starts without starting the lifetime of elements, e.g. by std::allocator::allocate() or implicitly, all the element subobjects still get created.

I can't find a direct confirmation of this, but consider e.g. that:

Is this interpretation correct? Is "creating an object" different from "starting its lifetime"? If yes, do we have wording that more explicitly confirms this?


Note that this is a question. I'm interested in what the standard has to say about it, this wording quirk likely doesn't affect what happens in practice.

18
  • 1
    I think [intro.object] and [basic.life] tells you everything. Commented Nov 17 at 9:36
  • Creation has an explicit definition. I'm not sure if there is a clear distinction between creating an object and starting its lifetime. However, it is clear from that definition that you can have allocated storage whose objects are not yet created. Commented Nov 17 at 9:40
  • 1
    @Homer512 Does it? It lists all scenarios that create objects, but it's not clear to me from this quote if "create" == "start lifetime" or not, since the scenarios listed do both. Commented Nov 17 at 10:24
  • @GKxx Care to write a full answer? :P Commented Nov 17 at 10:26
  • 1
    @LanguageLawyer Aha! This is worthy of an answer I think. :) Commented Nov 17 at 13:28

3 Answers 3

5

Based on the comment by @LanguageLawyer:

My understanding as explained in the question is mostly correct, but "created" isn't the right word for this, as it apparently means something else (something similar to starting the lifetime as @Caleb says in his answer).

So yes, beginning a lifetime1 of an object always recursively causes all its member (sub)objects to begin existing. "Existing" not necessarily in the sense of having their lifetime started, but in the wider sense of being considered "objects" (possibly outside of their lifetime).

(1 Or more correctly, this happens even before the lifetime starts, since e.g. class data members already exist while its constructor runs (and its lifetime only starts when the constructor finishes). So apparently "creating" an object means beginning the process that will start its lifetime, and creating an object recursively causes all its members to begin existing.)

E.g. when an array begins its lifetime but its elements don't (e.g. as returned by std::allocator::allocate()), its elements are still considered to be "objects", since an array by definition contains a bunch of objects (regardless of their lifetime):

[dcl.array]/6

An object of type “array of N U” consists of a contiguously allocated non-empty set of N subobjects of type U, known as the elements of the array...

Similarly all member objects of a union always exist, even inactive ones, though at most one of them can be within its lifetime at the same time.

Sign up to request clarification or add additional context in comments.

8 Comments

beginning a lifetime of an object always recursively causes all its member (sub)objects to begin existing Creating, not starting the lifetime
@LanguageLawyer Hm, are there any cases where this matters, i.e. where the top-level object could be created without having its lifetime started?
During construction, the lifetime of the class object is not yet started, but it has all of its subobjects
@LanguageLawyer Has the lifetime of a class object already started when (the body of) a delegating constructor is executed? I am not sure whether the initialization is considered as completed by finishing the target constructor run, or whether it is completed by finishing the delegating constructor run.
The lifetime of an object of class type specifically does not start until the constructor is complete. See basic.life#2 (quoted in my answer)
@Caleth But which constructor, the target one, or the delegating one? If a delegating constructor throws, the destructor is called, which ends the lifetime. So, if the lifetime within a delegating constructor hasn't started yet, how can it be ended? Or, is the destructor called in this case even when the lifetime hasn't started yet?
All of them. Lifetime starts after initialization is complete. If a constructor throws, the lifetime never starts, the destructor is called. Within the destructor the object is never within it's lifetime.
But a destructor isn't run when a constructor throws (for the same object). Destructors run for subobjects. Delegating constructors are an exception to this rule, and act like the object is its own subobject.
1

starting object's lifetime recursively creates all its subobjects

This used to be the case¹; only because there was no method to create an object but not its sub-objects. Your example of std::allocator::allocate is now one counterexample to that.

In general, the process of creating an object has some duration. The object's lifetime starts once that process is complete.

The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • its initialization (if any) is complete (including vacuous initialization) ([dcl.init]),

[basic.life#2]

So in that sense, "creating an object" is not the quite the same as "starting its lifetime", as that happens instantaneously at the end of the initialization. But that doesn't seem to be the confusion that your colleague has.

Further along in [basic.life#8], the standard is fairly precise in identifying values that correspond to a future (or past) object, i.e. the storage that will (or did) contain a particular object, outside that object's lifetime. It does still refer to the object, so there is still the notion of an object, even outside its lifetime. Importantly, within a constructor or destructor, this points to such an object.

Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways.


¹ Arguably it was never the case, because even at that time std::vector had to be able to transmute an array of one size into an array of another size whilst maintaining the lifetime of the sub-objects shared by the two arrays.

8 Comments

Hm, but the spec for std::allocate says that it returns a pointer to the first array element. I know the lifetime of the elements doesn't start, but the argument is that the element needs to exist in some sense for there to be a pointer to it, hence the idea that it's created without having its lifetime started. And similarly (another example from the Q) forming a pointer to an inactive enum member also requires it to exist in some sense, because the spec for & says it returns a pointer to an object.
You can have a pointer to an object who's lifetime hasn't started, I'm pretty sure this is one of those cases. std::construct_at takes a T * not a void *
Taking the address of a union member doesn't start it's lifetime. Only initialization and assignment do that.
Yeah, but this is the point. "Object who's lifetime hasn't started" - what object, if it doesn't exist? :P It's not in the process of being initialized either (you can have an array without the elements indefinitely). This idea about subobjects being created without their lifetime started answers the "what object" question.
basic.life #7 and #8 are all about objects who's lifetime hasn't started, so it's a concept the standard does deal with.
I'm not saying that it does, I'm saying that & can only return the address of an object according to the spec, therefore the inactive union member subobject has to exist in some sense (as an object that was created but who's lifetime hasn't started, if the theory in the Q is correct).
The alternate explanation is that it is UB to use an inactive union member (outside the carve-out for common initial sequences)
basic.life is explicit that & can be used once storage is obtained, even before the lifetime of an object has started. No mention is made there of "creation". So & can be used on inactive union members, since they have storage.
-1

I am not entirely sure if this answers your question in full, but here is some information to consider:

Both array and union are merely "managerial". They provide safety and structure, without actually caring much about their contents. They allocate the space needed to perform their duties, but the way they create their content is maybe not the most intuitive way. The closest to that is initializing it with default values.

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 ([conv.rval], [class.temporary]) [1]

If you have a simple std::array<int, n> numbers, then the array-object is created and it will allocate enough space to store n integers. Since these are arithmetic types, they will be set to 0 by default. Thus you could say the integer objects are created.

If you have std::array<SomePointerTypeObject, n> objects, then the array will still allocate space for n pointers, however all of them will be set to default nullptr. If you consider nullptr to be an object is another thing, but since it handles like any other pointer, i would argue by creating the array objects, you linked to the nullptr "object". Since it is a constant, it is not "created", but has been "created" at its time of definition.

Since union-objects behave similarly with their first named member, they are the same for this argument.

The properties of an object are determined when the object is created. An object can have a name ([basic.pre]). An object has a storage duration ([basic.stc]) which influences its lifetime ([basic.life]). [1]

Hope this helps, or at least brings information as a basis for further discussion.

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

1 Comment

Sorry, but this isn't what I asked about. E.g. for the arrays, I'm not talking about creating an array normally, because that of course does create its elements as well. I'm talking about the fake/formal array "creation" that doesn't actually perform any initialization of the elements, such as by std::allocator::allocate (which is basically a glorified malloc).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.