Date: Sun, 11 Jan 2026 11:36:56 -0500
> int main() {
> A *p = (A*)malloc(sizeof(A)); // that is, `A *p = (A*)(Aggr*)malloc(sizeof(Aggr))` without the extra steps
> p->store(42); // OK; malloc implicitly created an `Aggr`, so we can use its base `A`
> }
This is in [into.object/11]. I think the note explicitly disallows this.
```
Some operations are described as implicitly creating objects within a specified region of storage. For each
operation that is specified as implicitly creating objects, that operation implicitly creates and starts the
lifetime of zero or more objects of implicit-lifetime types (6.8.1) in its specified region of storage if doing
so would result in the program having defined behavior. If no such set of objects would give the program
defined behavior, the behavior of the program is undefined. If multiple such sets of objects would give the
program defined behavior, it is unspecified which such set of objects is created.
[Note 4: Such operations do not start the lifetimes of subobjects of such objects that are not themselves of implicit-
lifetime types. —end note]
```
> But you could replace `A` in that example with `std::string`, or `std::lock_guard<M>`, or any arbitrary type, and it would work exactly the same way. So (1) it's not a "real" workaround, it's just a workaround for the theology. (Remember that the physical reality doesn't need a workaround anyway — your code already Does The Right Thing in reality.) And (2) the theology is definitely broken.
I find myself disagreeing with you again, which most likely means I'm going to be proven double-wrong. My appetite for public ridicule seems to know no bounds.
But, we have this from [class.prop/16]
```
A class S is an implicit-lifetime class if
— it is an aggregate whose destructor is not user-provided or
— it has at least one trivial eligible constructor and a trivial, non-deleted destructor.
```
The aggregate must have a destructor that is not user-provided, and things like std::string certainly don't qualify because either through inheritance or composition this would be a violation.
Now, I'll go put on my lead undergarments in preparation for the reply.
Jody
> On Jan 11, 2026, at 10:57 AM, Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]> wrote:
>
> On Sun, Jan 11, 2026 at 10:07 AM Jody Hagins <coachhagins_at_[hidden] <mailto:coachhagins_at_[hidden]>> wrote:
>> [...]
>>> No, that claim I can show to be incorrect. Here `Foo` is 100% definitely an aggregate...
>>
>> [... Hmm, Clang 21 behaves differently from Clang trunk on a different example ...]
>> Look here <https://godbolt.org/z/9cshPonz9> for something a bit different.
>> Clang 21 has a very different implementation of `__builtin_is_implicit_lifetime` than trunk, especially regarding what qualifies as a "trivial eligible constructor."
>
> Aha. FWIW, when I wrote my email, I wrongly believed that no compiler had implemented is_implicit_lifetime yet, because when I tried to use the compiler intrinsic __is_implicit_lifetime(T), it just gave me a syntax error. Thanks to your example, I discovered that very recently Clang, GCC, and MSVC all decided to start "moving fast and breaking things" by renaming all their compiler intrinsics to include the word __builtin at the front <https://github.com/llvm/llvm-project/pull/101807#discussion_r1703144883>. So now it's spelled __builtin_is_implicit_lifetime(T). Anyone branching on __has_builtin(__is_implicit_lifetime) is waiting for a train that will never come!
>
>> [... Clang 21 had a bug with constructors that would-have-been-trivial-if-they-weren't-deleted ...]
>> [...] You said maybe the real issue was option (D) — that the definition of implicit-lifetime itself might need adjustment, not that we need new constructor syntax. And here was libc++ apparently agreeing!
>> Their interpretation seemed to be: "If the object representation could theoretically be trivially constructed — if it's just bytes that don't need magic initialization — then it's implicit-lifetime, regardless of what constructors are actually declared."
>
> Well, no, that's not what it means, either. Because, remember, all aggregates are implicit-lifetime, no matter what they contain.
> Actually, a silly language-lawyer answer is that you could "work around" your abstract-machine problem like this: Instead of—
>
> using A = std::atomic<int>; static_assert(not std::is_implicit_lifetime_v<A>);
> int main() {
> A *p = (A*)malloc(sizeof(A));
> p->store(42); // technically UB, because no `A` exists at that location yet (malloc didn't implicitly create one for us)
> }
>
> —you could just write—
>
> using A = std::atomic<int>; static_assert(not std::is_implicit_lifetime_v<A>);
> struct Aggr : A {};
> static_assert(std::is_aggregate_v<Aggr>); // guaranteed
> static_assert(std::is_implicit_lifetime_v<Aggr>); // guaranteed
> static_assert(sizeof(Aggr) == sizeof(A)); // highly likely in practice
> static_assert(std::is_pointer_interconvertible_base_of_v<A, Aggr>); // guaranteed, I think
>
> int main() {
> A *p = (A*)malloc(sizeof(A)); // that is, `A *p = (A*)(Aggr*)malloc(sizeof(Aggr))` without the extra steps
> p->store(42); // OK; malloc implicitly created an `Aggr`, so we can use its base `A`
> }
>
> The language-lawyer question is whether the abstract machine is required to make this code work even without seeing the definition of `struct Aggr`. I think it probably is.
>
> But you could replace `A` in that example with `std::string`, or `std::lock_guard<M>`, or any arbitrary type, and it would work exactly the same way. So (1) it's not a "real" workaround, it's just a workaround for the theology. (Remember that the physical reality doesn't need a workaround anyway — your code already Does The Right Thing in reality.) And (2) the theology is definitely broken.
>
>> [...]
>> Clang 20 and 21 shipped with an implementation that effectively asked: "Could this type's representation be trivially constructed, ignoring what constructors are actually declared?"
>
> Well, that's literally always true, if you go far enough down the stack. For example, a std::string is just a couple of pointers and/or integers, and both pointers and integers can be trivially constructed.
> Really what `is_implicit_lifetime` is trying to answer, physically, is the inverse of `has_unique_object_representations`: it's trying to answer "If I take an object of this type and put garbage bits into its entire object representation, does it still satisfy all its class invariants? Can I take that garbage object and use it as if it were a normally constructed object?" For types like `int` and `atomic<int>`, the answer is "yes" (except on platforms where `int` has trap representations, where `malloc` could return bytes that happened to form such a representation: then using that `int` would explode). For types like `std::string`, the answer is definitely "no."
> The current paper spec doesn't achieve that goal at all, but that's the goal it's aiming for, in physical terms.
>
> ...Although `is_implicit_lifetime` also has a second use. It's kinda-sorta the above "Can I assume there's an object here when there's not, really?" but it's also kinda-sorta "Can I tell the abstract machine that there's already an object here because I know there is?" — which is std::start_lifetime_as<T> <https://www.cppreference.com/w/cpp/memory/start_lifetime_as.html>. (`start_lifetime_as` requires T to be an implicit-lifetime type. It almost certainly shouldn't.) These aren't really the same use-case at all, but I don't think we realized it until `start_lifetime_as` had already shipped. Perils of focusing on the abstract machine instead of on physical reality...
>
> my $.02,
> –Arthur
> A *p = (A*)malloc(sizeof(A)); // that is, `A *p = (A*)(Aggr*)malloc(sizeof(Aggr))` without the extra steps
> p->store(42); // OK; malloc implicitly created an `Aggr`, so we can use its base `A`
> }
This is in [into.object/11]. I think the note explicitly disallows this.
```
Some operations are described as implicitly creating objects within a specified region of storage. For each
operation that is specified as implicitly creating objects, that operation implicitly creates and starts the
lifetime of zero or more objects of implicit-lifetime types (6.8.1) in its specified region of storage if doing
so would result in the program having defined behavior. If no such set of objects would give the program
defined behavior, the behavior of the program is undefined. If multiple such sets of objects would give the
program defined behavior, it is unspecified which such set of objects is created.
[Note 4: Such operations do not start the lifetimes of subobjects of such objects that are not themselves of implicit-
lifetime types. —end note]
```
> But you could replace `A` in that example with `std::string`, or `std::lock_guard<M>`, or any arbitrary type, and it would work exactly the same way. So (1) it's not a "real" workaround, it's just a workaround for the theology. (Remember that the physical reality doesn't need a workaround anyway — your code already Does The Right Thing in reality.) And (2) the theology is definitely broken.
I find myself disagreeing with you again, which most likely means I'm going to be proven double-wrong. My appetite for public ridicule seems to know no bounds.
But, we have this from [class.prop/16]
```
A class S is an implicit-lifetime class if
— it is an aggregate whose destructor is not user-provided or
— it has at least one trivial eligible constructor and a trivial, non-deleted destructor.
```
The aggregate must have a destructor that is not user-provided, and things like std::string certainly don't qualify because either through inheritance or composition this would be a violation.
Now, I'll go put on my lead undergarments in preparation for the reply.
Jody
> On Jan 11, 2026, at 10:57 AM, Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]> wrote:
>
> On Sun, Jan 11, 2026 at 10:07 AM Jody Hagins <coachhagins_at_[hidden] <mailto:coachhagins_at_[hidden]>> wrote:
>> [...]
>>> No, that claim I can show to be incorrect. Here `Foo` is 100% definitely an aggregate...
>>
>> [... Hmm, Clang 21 behaves differently from Clang trunk on a different example ...]
>> Look here <https://godbolt.org/z/9cshPonz9> for something a bit different.
>> Clang 21 has a very different implementation of `__builtin_is_implicit_lifetime` than trunk, especially regarding what qualifies as a "trivial eligible constructor."
>
> Aha. FWIW, when I wrote my email, I wrongly believed that no compiler had implemented is_implicit_lifetime yet, because when I tried to use the compiler intrinsic __is_implicit_lifetime(T), it just gave me a syntax error. Thanks to your example, I discovered that very recently Clang, GCC, and MSVC all decided to start "moving fast and breaking things" by renaming all their compiler intrinsics to include the word __builtin at the front <https://github.com/llvm/llvm-project/pull/101807#discussion_r1703144883>. So now it's spelled __builtin_is_implicit_lifetime(T). Anyone branching on __has_builtin(__is_implicit_lifetime) is waiting for a train that will never come!
>
>> [... Clang 21 had a bug with constructors that would-have-been-trivial-if-they-weren't-deleted ...]
>> [...] You said maybe the real issue was option (D) — that the definition of implicit-lifetime itself might need adjustment, not that we need new constructor syntax. And here was libc++ apparently agreeing!
>> Their interpretation seemed to be: "If the object representation could theoretically be trivially constructed — if it's just bytes that don't need magic initialization — then it's implicit-lifetime, regardless of what constructors are actually declared."
>
> Well, no, that's not what it means, either. Because, remember, all aggregates are implicit-lifetime, no matter what they contain.
> Actually, a silly language-lawyer answer is that you could "work around" your abstract-machine problem like this: Instead of—
>
> using A = std::atomic<int>; static_assert(not std::is_implicit_lifetime_v<A>);
> int main() {
> A *p = (A*)malloc(sizeof(A));
> p->store(42); // technically UB, because no `A` exists at that location yet (malloc didn't implicitly create one for us)
> }
>
> —you could just write—
>
> using A = std::atomic<int>; static_assert(not std::is_implicit_lifetime_v<A>);
> struct Aggr : A {};
> static_assert(std::is_aggregate_v<Aggr>); // guaranteed
> static_assert(std::is_implicit_lifetime_v<Aggr>); // guaranteed
> static_assert(sizeof(Aggr) == sizeof(A)); // highly likely in practice
> static_assert(std::is_pointer_interconvertible_base_of_v<A, Aggr>); // guaranteed, I think
>
> int main() {
> A *p = (A*)malloc(sizeof(A)); // that is, `A *p = (A*)(Aggr*)malloc(sizeof(Aggr))` without the extra steps
> p->store(42); // OK; malloc implicitly created an `Aggr`, so we can use its base `A`
> }
>
> The language-lawyer question is whether the abstract machine is required to make this code work even without seeing the definition of `struct Aggr`. I think it probably is.
>
> But you could replace `A` in that example with `std::string`, or `std::lock_guard<M>`, or any arbitrary type, and it would work exactly the same way. So (1) it's not a "real" workaround, it's just a workaround for the theology. (Remember that the physical reality doesn't need a workaround anyway — your code already Does The Right Thing in reality.) And (2) the theology is definitely broken.
>
>> [...]
>> Clang 20 and 21 shipped with an implementation that effectively asked: "Could this type's representation be trivially constructed, ignoring what constructors are actually declared?"
>
> Well, that's literally always true, if you go far enough down the stack. For example, a std::string is just a couple of pointers and/or integers, and both pointers and integers can be trivially constructed.
> Really what `is_implicit_lifetime` is trying to answer, physically, is the inverse of `has_unique_object_representations`: it's trying to answer "If I take an object of this type and put garbage bits into its entire object representation, does it still satisfy all its class invariants? Can I take that garbage object and use it as if it were a normally constructed object?" For types like `int` and `atomic<int>`, the answer is "yes" (except on platforms where `int` has trap representations, where `malloc` could return bytes that happened to form such a representation: then using that `int` would explode). For types like `std::string`, the answer is definitely "no."
> The current paper spec doesn't achieve that goal at all, but that's the goal it's aiming for, in physical terms.
>
> ...Although `is_implicit_lifetime` also has a second use. It's kinda-sorta the above "Can I assume there's an object here when there's not, really?" but it's also kinda-sorta "Can I tell the abstract machine that there's already an object here because I know there is?" — which is std::start_lifetime_as<T> <https://www.cppreference.com/w/cpp/memory/start_lifetime_as.html>. (`start_lifetime_as` requires T to be an implicit-lifetime type. It almost certainly shouldn't.) These aren't really the same use-case at all, but I don't think we realized it until `start_lifetime_as` had already shipped. Perils of focusing on the abstract machine instead of on physical reality...
>
> my $.02,
> –Arthur
Received on 2026-01-11 16:37:12
