C++ Logo

std-proposals

Advanced search

Re: [std-proposals] User-Defined Trivial Constructors

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Sun, 11 Jan 2026 10:57:23 -0500
On Sun, Jan 11, 2026 at 10:07 AM Jody Hagins <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 15:57:38