An aggregate is implicit lifetime? Doesn't seem right
Asked Answered
O

1

8

According to this and this an aggregate is implicit lifetime.

A class S is an implicit-lifetime class if it is an aggregate or has at least one trivial eligible constructor and a trivial, non-deleted destructor.

And implicit lifetime objects can be created with malloc. See this example.

#include <cstdlib>
struct X { int a, b; };

X* MakeX()
{
    // One of possible defined behaviors:
    // the call to std::malloc implicitly creates an object of type X
    // and its subobjects a and b, and returns a pointer to that X object
    X* p = static_cast<X*>(std::malloc(sizeof(X)));
    p->a = 1;
    p->b = 2;
   return p;
}

But struct A {std::string s;}; is also an aggregate. But this produces an exception as I expected since the assignment to s would first destruct s and s is not valid as it was never constructed.

#include <cstdlib>
#include <string>
#include <iostream>

struct X { std::string s; };

X* MakeX()
{
    X* p = static_cast<X*>(std::malloc(sizeof(X)));
    p->s = "abc";
    return p;
}

int main()
{
    static_assert(std::is_aggregate_v<X>);
    auto x = MakeX();
    std::cout << x->s << "\n";
}

So why is an aggregate considered an implicit lifetime type?

Oxbridge answered 11/3, 2023 at 20:48 Comment(10)
You shouldn't expect an exception, the second example is just undefined behaviour. With some combinations of malloc/STL implementations this might just work fine.Jasun
@Jasun I wasn't surprised to get an exception knowing how the string handle is made in MSVC's library. But the exception is an unsurprising result of UB which is clearly happening.Oxbridge
My mental model of an aggregate that is implicit lifetime is one where all of it's components have trivial destructors.Oxbridge
From: Lifetime "...If a subobject of an implicitly created object is not of an implicit-lifetime type, its lifetime does not begin implicitly...." means that even though struct X is an aggrigate; the lifetime of std::string s; has not been started (still working through various standard changes).Roadhouse
@RichardCritten I agree, I just don't see that stated in the standard. And it's a rather bald statement to say that an aggregate is implicit lifetime.Oxbridge
Have a look at [intro.object note 4] "...Such operations do not start the lifetimes of subobjects of such objects that are not themselves of implicit-lifetime types..."Roadhouse
And here the Standard for implicit life time classes [class.prop.9] "...A class S is an implicit-lifetime class if...it is an aggregate whose destructor is not user-provided or..."Roadhouse
@RichardCritten Thanks for the link to note 4. However, notes aren't part of the spec. And "an aggregate whose destructor is not user-provided" the example aggregate does not have a user provided dtor.Oxbridge
@Oxbridge I think you meant to say that they're not normative, that they don't specify rules. Which is true, they're there to provide adding clarifying information/context - like here, where that note is clarifying for you that the s subobject does not have its lifetime started (because string is not implicit-lifetime).Exeter
@Exeter Agreed. Mostly I'm just annoyed with the wording of aggregates being implicit lifetime types. There is no way that aggregate's lifetime was started. as @user17732522's answer pointed out, a construct_at is required to start the lifetime of a contained object.Oxbridge
U
4

Yes, as an aggregate X is an implicit-lifetime type (see [class.prop]/9), but importantly its member s is not an implicitly-lifetime type (see same reference, e.g. because std::string has a non-trivial destructor).

The consequence is that std::malloc(sizeof(X)) will implicitly create a X object and start its lifetime, but it will not start the lifetime of the s subobject of that X object. This follows from [intro.object]/10 specifying that it starts lifetime of implicit-lifetime objects, but not specifying anything about non-implicit-lifetime (sub-)objects. There is also nothing else in the standard that would specify starting the lifetime of these subobjects as e.g. rules for constructor calls and aggregate initialization would.

Therefore p->s = "abc"; still causes undefined behavior for accessing an object outside its lifetime.

You must explicitly start the lifetime of the s subobject first after malloc, e.g. with:

std::construct_at(&p->s);

After that, you can do p->s = "abc";.

Unmake answered 11/3, 2023 at 21:31 Comment(3)
Yep. My thinking too. std::construct_at starts the lifetime of s. Bizarre that the aggregate can be said to be implicit lifetime when all of it's components may not be. Likely to be confusing to new programmers.Oxbridge
@Oxbridge There would be no point to make the rule more restrictive. If aggregates containing non-implicit-lifetime types would not be implicit-lifetime, then malloc could not return a pointer to the implicitly created object. And specifying that the lifetime of the implicitly-created aggregate isn't started in this case would just be an unnecessary restriction that makes otherwise valid code invalid. Both std::construct_at(p) and std::construct_at(&p->s); are fine now, but with a stricter rule only the first would be.Unmake
Sure, but just stating that an aggregate is implicit lifetime w/o clearly stating, at the same place, that components may not be implicit lifetime, can be misleading. But I agree in that it's obvious malloc can't start the lifetime of things like vector and string. Malloc really just returns storage that has a valid ptr even if subobjects did not have their lifetime started. Stating this would not make it more restrictive, but would make it more clear. Just saying an aggregate is implicit lifetime suggests the object's contents lifetime is also implicit.Oxbridge

© 2022 - 2025 — McMap. All rights reserved.