[...]
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 ...]
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. 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>. (`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