Date: Sat, 10 Jan 2026 11:08:40 -0500
Is there going to be an equivalent "User-Defined Trivial Constructors"?
https://en.cppreference.com/w/cpp/types/is_implicit_lifetime.html
// The following types are collectively called implicit-lifetime types:
// * scalar types:
// * arithmetic types
// * enumeration types
// * pointer types
// * pointer-to-member types
// * std::nullptr_t
*// * implicit-lifetime class types// * is an aggregate whose
destructor is not user-provided// * has at least one trivial eligible
constructor and a trivial,// non-deleted destructor*
// * array types
// * cv-qualified versions of these types.
Copilot Search: "C++" "implicit lifetime type"
In C++20 and later, an implicit lifetime type is a type whose objects can
be created and destroyed without explicitly running a constructor *or
destructor* — their lifetime can begin simply by writing to their storage
(e.g., via memcpy, placement new, or std::start_lifetime_as), and end when
the storage is reused or released.
```cpp
struct Foo {
int value_;
// Safe: always initializes
Foo() : value_{0} {}
// Trivial: enables implicit lifetime
explicit Foo(std::trivial_t) = default;
~Foo() : value_{42} {} = default;
};
```
Think secure_string or secure_array clearing out memory in a specific way.
On Sat, Jan 10, 2026 at 11:03 AM Jarrad Waterloo <descender76_at_[hidden]>
wrote:
> Is there going to be an equivalent "User-Defined Trivial Constructors"?
>
> https://en.cppreference.com/w/cpp/types/is_implicit_lifetime.html
>
> // The following types are collectively called implicit-lifetime types:
> // * scalar types:
> // * arithmetic types
> // * enumeration types
> // * pointer types
> // * pointer-to-member types
> // * std::nullptr_t
>
>
>
> *// * implicit-lifetime class types// * is an aggregate whose
> destructor is not user-provided// * has at least one trivial eligible
> constructor and a trivial,// non-deleted destructor*
> // * array types
> // * cv-qualified versions of these types.
>
>
> Copilot Search: "C++" "implicit lifetime type"
>
> In C++20 and later, an implicit lifetime type is a type whose objects can
> be created and destroyed without explicitly running a constructor or
> destructor — their lifetime can begin simply by writing to their storage
> (e.g., via memcpy, placement new, or std::start_lifetime_as), and end when
> the storage is reused or released.
>
>
>
>
>
>
>
> On Sat, Jan 10, 2026 at 9:09 AM Jody Hagins via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> I'm looking for feedback on this proposal. It is appended in markdown
>> format.
>>
>> Thanks.
>>
>>
>> ---
>> title: "User-Defined Trivial Constructors"
>> document: Dxxxxr0
>> date: 2026-01-10
>> audience: EWG, LEWG
>> author:
>> - name: Jody Hagins
>> email: <coachhagins_at_[hidden]>
>> toc: true
>> ---
>>
>> # Abstract
>>
>> This paper proposes allowing `= default` on user-defined constructors,
>> enabling user-defined trivial constructors. This resolves a design tension
>> where types cannot simultaneously provide safe default initialization AND
>> qualify as implicit lifetime types. The primary motivation is
>> `std::atomic`, which C++20 inadvertently rendered unusable in shared memory
>> scenarios, but the solution benefits any type facing this trilemma.
>>
>> # The Helpful Neighbor Problem
>>
>> You know that neighbor. The one who "fixes" things.
>>
>> You have a fenced in back yard, and your dog hangs out there while you
>> are at work.
>>
>> However, the latch is a bit low, and the dog has figured out how to get
>> out on his own, because he's smart and he's a dog, not a cat.
>> The cat would just climb the tree next to the fence.
>>
>> Helpful Neighbor, bless his heart, decides to solve this problem while
>> you're at work. He welds the gate shut. Problem solved! The dog can't
>> escape anymore.
>>
>> Of course, now *you* can't get into your own backyard either. But hey,
>> the dog's contained.
>>
>> When you point this out, Helpful Neighbor offers solutions: you could
>> climb over the fence (inconvenient), or you could install a new gate that
>> only opens with a 47-step authentication process (safe but unusable), or
>> you could just accept that the backyard is now dog-only territory (give up).
>>
>> What you *actually* needed was a latch that works for humans but not
>> dogs. A solution that achieves the safety goal without sacrificing basic
>> functionality.
>>
>> C++20 had a Helpful Neighbor moment with `std::atomic`.
>>
>> # Motivation
>>
>> ## The Trilemma
>>
>> Consider a type author who wants to create a type that:
>>
>> 1. **Has safe default initialization** — objects are never left
>> uninitialized
>> 2. **Is an implicit lifetime type** — can be used in shared memory,
>> memory-mapped files, or with `std::start_lifetime_as`
>> 3. **Cannot be copied or moved** — the type semantically represents a
>> unique resource
>>
>> Today, it's near impossible.
>>
>> To be an implicit lifetime type, a class must, among other things, have
>> at least one trivial constructor. The only way to explicitly specify a
>> trivial constructor is via `= default`. But you can't only use `= default`
>> user-defined constructors. Hence, no way for anything but the default,
>> copy, and move constructors to be trivial.
>>
>> If you delete copy and move (requirement 3), only the default constructor
>> remains eligible to be trivial. But if you make the default constructor
>> trivial (requirement 2), objects can be left uninitialized (violating
>> requirement 1).
>>
>> Pick any two. You can't have all three.
>>
>> | Choice | What You Get | What You Lose |
>> |--------|--------------|---------------|
>> | Safe default ctor | Objects always initialized | Not an implicit
>> lifetime type |
>> | Trivial default ctor | Implicit lifetime type | Uninitialized objects
>> possible |
>> | No default ctor | No uninitialized objects | No `Foo x;` or `Foo x{};`
>> syntax |
>>
>> This isn't a theoretical concern. It's the exact situation `std::atomic`
>> finds itself in after C++20.
>>
>> ## Real-World Impact
>>
>> The implicit lifetime type requirement isn't academic. It's essential for:
>>
>> - **Shared memory** — inter-process communication via memory-mapped
>> regions
>> - **Memory-mapped files** — persistent data structures
>> - **Custom allocators** — placement new into raw storage
>> - **`std::start_lifetime_as`** — explicit lifetime management
>>
>> Every C++ message queue library uses atomics in shared memory. Lock-free
>> data structures in shared memory are a common pattern. All of this code is
>> now, technically speaking, undefined behavior.
>>
>> The standard even explicitly contemplates cross-process atomics.
>> [atomics.lockfree] discusses lock-free atomics working correctly: "This
>> restriction enables communication by memory that is mapped into a process
>> more than once and by memory that is shared between two processes."
>> Except now, getting a `std::atomic` *into* shared memory with defined
>> behavior is impossible.
>>
>> "But it works!" you might say. And you'd be right — today. Compilers are
>> getting smarter. Optimizers are getting more aggressive. Undefined behavior
>> that "works" today may not work tomorrow. We've all seen this movie before.
>>
>> # The `std::atomic` Story
>>
>> ## What Happened
>>
>> In C++20, P0883R2 "Fixing Atomic Initialization" changed `std::atomic`'s
>> default constructor. Previously, it was trivial (did nothing). After P0883,
>> it explicitly zero-initializes.
>>
>> The motivation was reasonable: users writing `std::atomic<int> counter;`
>> would get an uninitialized atomic, which is almost certainly a bug. Making
>> the default constructor zero-initialize catches this class of errors.
>>
>> But, in that same C++20 cycle, P0593R6 "Implicit creation of objects for
>> low-level object manipulation" formalized implicit lifetime types,
>> including the part about trivial constructors.
>>
>> The result? C++20 simultaneously:
>>
>> 1. Defined what implicit lifetime types are
>> 2. Made `std::atomic` not be one
>>
>> ## The Constructor Situation
>>
>> Let's look at `std::atomic`'s constructors prior to C++20:
>>
>> | Constructor | Status | Trivial? |
>> |-------------|--------|----------|
>> | Default | = default | Yes, if T is trivial |
>> | Copy | Deleted | N/A |
>> | Move | Deleted | N/A |
>> | Value | Takes a T | No (user-defined) |
>>
>> This type is an implicit lifetime type, according to the definition
>> drafted for C++20.
>>
>> And the is `std::atomic`'s constructors after C++20:
>>
>> | Constructor | Status | Trivial? |
>> |-------------|--------|----------|
>> | Default | Zero-initializes | No |
>> | Copy | Deleted | N/A |
>> | Move | Deleted | N/A |
>> | Value | Takes a T | No (user-defined) |
>>
>> Zero trivial constructors. Not an implicit lifetime type. No way to use
>> it in shared memory with defined behavior.
>>
>> The P0883 authors had good intentions. They saw a safety problem and
>> fixed it. But like our Helpful Neighbor, they welded the gate shut in the
>> process.
>>
>> ## Why Not Just Undo It?
>>
>> We could revert P0883's change to the default constructor. But that
>> brings back uninitialized atomics, which genuinely is a footgun.
>>
>> We could make the default constructor private (and trivial).
>> This would satisfy implicit lifetime requirements because there is a
>> trivial default constructor, even though it is private.
>> But, this would prevent accidentally creating an uninitialized instance
>> since `std::atomic<int> x;` would no longer compile.
>> Unfortunately, neither would `std::atomic<int> x;{}`.
>>
>> While a compile error is better than silent uninitialization, it's a
>> breaking change, and it's still inconvenient.
>>
>> What we need is a way to have *both*: a safe default constructor *and* a
>> trivial constructor for implicit lifetime purposes. The language currently
>> doesn't allow this.
>>
>> # Proposed Solution
>>
>> ## User-Defined Trivial Constructors
>>
>> We propose allowing `= default` on constructors that are not special
>> member functions:
>>
>> ```cpp
>> struct Foo {
>> int value_;
>>
>> // Safe: always initializes
>> Foo() : value_{0} {}
>>
>> // Trivial: enables implicit lifetime
>> explicit Foo(std::trivial_t) = default;
>> };
>> ```
>>
>> The semantics are straightforward:
>>
>> - `= default` on a user-defined constructor means "do nothing"
>> - It is trivial — exactly what `Foo() = default` would do
>> - The parameter exists only for overload resolution
>> - It is only valid if `Foo() = default` would produce a trivial default
>> constructor
>> - It must be marked `explicit` to prevent unintended use and because
>> `explicit` should be the default, but that's not a nice story about my dog
>> and my neighbor.
>>
>> ## Usage
>>
>> ```cpp
>> // Normal usage — safe, always initialized
>> Foo regular;
>> Foo also_regular{};
>>
>> // Explicit trivial construction — for shared memory scenarios
>> // where the object is constructed and manually initialized later.
>> Foo in_shared_memory(std::trivial);
>> ```
>>
>> In practice, the trivial constructor may never be called directly. Its
>> existence is what matters — it makes the type an implicit lifetime type,
>> enabling `std::start_lifetime_as`, `mmap`, and similar operations to
>> implicitly begin the object's lifetime.
>>
>> ## Impact on `std::atomic`
>>
>> With this feature, `std::atomic` can be fixed with zero breaking changes:
>>
>> ```cpp
>> template<typename T>
>> struct atomic {
>> // ... existing members ...
>>
>> // Existing: safe zero-initializing default ctor
>> atomic() noexcept : value_{} {}
>>
>> // New: trivial ctor for implicit lifetime
>> explicit atomic(std::trivial_t) = default;
>> };
>> ```
>>
>>
>> Existing code continues to work exactly as before. The only change is
>> that `std::atomic<int>` is now, once again, an implicit lifetime type.
>>
>> # Alternative Solutions Considered
>>
>> There are numerous options for addressing the `std::atomic` problem, as
>> summarized below.
>>
>> These are, it happens, the same options users have for their own types,
>> many of which have been in use for decades.
>>
>> For example, the Boost.Interprocess types all fall into this same
>> category.
>> They are used within shared memory, and some require explicit
>> construction.
>> They sit on top of native builtin types that could be implicitly created,
>> but there is no explicit way to specify this.
>>
>>
>> ## Committee Fiat
>>
>> The committee could simply declare that `std::atomic<T>` is an implicit
>> lifetime type for lock-free built-in types, rules be damned.
>>
>> This works for `std::atomic` but sets a troubling precedent.
>> It doesn't help user-defined types facing the same trilemma.
>> And "we'll just special-case it" isn't a scalable design philosophy.
>>
>> ## New Atomic Type
>>
>> We could introduce a new type, `std::basic_atomic` or similar, that
>> maintains trivial constructors.
>>
>> This splits the ecosystem. Library authors would need to decide which
>> atomic to use. Existing code using `std::atomic` in shared memory remains
>> broken. And again, it doesn't solve the general problem.
>>
>> ## `std::atomic_ref`
>>
>> "Just use `std::atomic_ref` with raw integer types in shared memory."
>>
>> This is viable, but:
>>
>> - Existing code is still undefined behavior
>> - It requires discipline: *all* accesses must go through `atomic_ref`
>> - The standard explicitly states: "While any `atomic_ref` instances exist
>> that reference the `*ptr` object, all accesses to that object shall
>> exclusively occur through those `atomic_ref` instances"
>> - One direct access without the wrapper is undefined behavior
>> - This is easy to get wrong
>>
>> ## Private Trivial Default Constructor
>>
>> ```cpp
>> template<typename T>
>> struct atomic {
>> private:
>> atomic() = default; // Trivial, satisfies implicit lifetime
>> public:
>> // Users must explicitly initialize
>> atomic(T desired) noexcept : value_(desired) {}
>> };
>> ```
>>
>> This achieves implicit lifetime status and prevents uninitialized atomics
>> (the old `std::atomic<int> x;` becomes a compile error). But it's a
>> breaking change — existing code using `std::atomic<int> x{};` would fail.
>>
>> Without a language change, this is the best we can get.
>> It's inconvenient for all, and requires lots of code change to adopt, but
>> it works.
>>
>> ## Why User-Defined Trivial Constructors Win
>>
>> The proposed solution:
>>
>> - **Solves the general problem** — any type can benefit, not just
>> `std::atomic`
>> - **Is non-breaking** — existing code continues to work
>> - **Is principled** — no special-case magic, just a natural extension of
>> `= default`
>> - **Achieves both goals** — safe default initialization AND implicit
>> lifetime eligibility
>>
>> # Design Decisions
>>
>> ## Tag Type
>>
>> The proposal uses a standard-provided tag type `std::trivial_t` (with a
>> corresponding `std::trivial` value).
>> This ensures uniform usage across the ecosystem.
>> I'm not in love with the name, and would happily consider alternatives.
>>
>> An alternative design would allow *any* constructor signature to use `=
>> default`.
>> This provides maximum flexibility because we don't know what the future
>> holds, but may be more than necessary.
>> The tag type approach is simpler to specify and makes intent clear.
>>
>> We're open to either approach — this is an area where committee feedback
>> would be valuable.
>>
>> ## Why `explicit`?
>>
>> The trivial constructor should be marked `explicit` to prevent accidental
>> use:
>>
>> ```cpp
>> void takes_foo(Foo);
>>
>> takes_foo({}); // Uses default ctor — safe
>> takes_foo(std::trivial); // Compile error — explicit required
>> takes_foo(Foo(std::trivial)); // OK — intentional
>> ```
>>
>> This ensures developers don't accidentally create uninitialized objects
>> through implicit conversions.
>>
>> Not to mention, that `explicit` should be the default for constructors,
>> and you must say `explicit(false)` to get an implicit one, but that doesn't
>> fit with a cozy story about my dog and my neighbor.
>>
>> ## Constraint: Must Be Trivial
>>
>> The feature is only valid when the equivalent `Foo() = default` would be
>> trivial.
>> If a class has members with non-trivial default constructors,
>> `Foo(std::trivial_t) = default` is ill-formed.
>>
>> This prevents misuse — you can't create a "trivial" constructor that
>> actually isn't.
>>
>> # Implementation Experience
>>
>> [TODO: Seek implementation experience from compiler vendors]
>>
>> # Wording
>>
>> [TODO: Detailed wording changes. Key areas:]
>>
>> - Extend [class.default.ctor] or create new section for defaulted
>> non-special-member constructors
>> - Define when such a constructor is trivial
>> - Add `std::trivial_t` to `<type_traits>` or a new header
>> - Specify `is_trivially_constructible` behavior
>> - Update `std::atomic` specification
>> - Others???
>>
>> # Acknowledgments
>>
>> [TODO]
>>
>> # References
>>
>> - P0883R2 "Fixing Atomic Initialization"
>> - P0593R6 "Implicit creation of objects for low-level object manipulation"
>> - [basic.life] Object lifetime
>> - [class.default.ctor] Default constructors
>> - [atomics.lockfree] Lock-free property
>> --
>> Std-Proposals mailing list
>> Std-Proposals_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>
>
https://en.cppreference.com/w/cpp/types/is_implicit_lifetime.html
// The following types are collectively called implicit-lifetime types:
// * scalar types:
// * arithmetic types
// * enumeration types
// * pointer types
// * pointer-to-member types
// * std::nullptr_t
*// * implicit-lifetime class types// * is an aggregate whose
destructor is not user-provided// * has at least one trivial eligible
constructor and a trivial,// non-deleted destructor*
// * array types
// * cv-qualified versions of these types.
Copilot Search: "C++" "implicit lifetime type"
In C++20 and later, an implicit lifetime type is a type whose objects can
be created and destroyed without explicitly running a constructor *or
destructor* — their lifetime can begin simply by writing to their storage
(e.g., via memcpy, placement new, or std::start_lifetime_as), and end when
the storage is reused or released.
```cpp
struct Foo {
int value_;
// Safe: always initializes
Foo() : value_{0} {}
// Trivial: enables implicit lifetime
explicit Foo(std::trivial_t) = default;
~Foo() : value_{42} {} = default;
};
```
Think secure_string or secure_array clearing out memory in a specific way.
On Sat, Jan 10, 2026 at 11:03 AM Jarrad Waterloo <descender76_at_[hidden]>
wrote:
> Is there going to be an equivalent "User-Defined Trivial Constructors"?
>
> https://en.cppreference.com/w/cpp/types/is_implicit_lifetime.html
>
> // The following types are collectively called implicit-lifetime types:
> // * scalar types:
> // * arithmetic types
> // * enumeration types
> // * pointer types
> // * pointer-to-member types
> // * std::nullptr_t
>
>
>
> *// * implicit-lifetime class types// * is an aggregate whose
> destructor is not user-provided// * has at least one trivial eligible
> constructor and a trivial,// non-deleted destructor*
> // * array types
> // * cv-qualified versions of these types.
>
>
> Copilot Search: "C++" "implicit lifetime type"
>
> In C++20 and later, an implicit lifetime type is a type whose objects can
> be created and destroyed without explicitly running a constructor or
> destructor — their lifetime can begin simply by writing to their storage
> (e.g., via memcpy, placement new, or std::start_lifetime_as), and end when
> the storage is reused or released.
>
>
>
>
>
>
>
> On Sat, Jan 10, 2026 at 9:09 AM Jody Hagins via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> I'm looking for feedback on this proposal. It is appended in markdown
>> format.
>>
>> Thanks.
>>
>>
>> ---
>> title: "User-Defined Trivial Constructors"
>> document: Dxxxxr0
>> date: 2026-01-10
>> audience: EWG, LEWG
>> author:
>> - name: Jody Hagins
>> email: <coachhagins_at_[hidden]>
>> toc: true
>> ---
>>
>> # Abstract
>>
>> This paper proposes allowing `= default` on user-defined constructors,
>> enabling user-defined trivial constructors. This resolves a design tension
>> where types cannot simultaneously provide safe default initialization AND
>> qualify as implicit lifetime types. The primary motivation is
>> `std::atomic`, which C++20 inadvertently rendered unusable in shared memory
>> scenarios, but the solution benefits any type facing this trilemma.
>>
>> # The Helpful Neighbor Problem
>>
>> You know that neighbor. The one who "fixes" things.
>>
>> You have a fenced in back yard, and your dog hangs out there while you
>> are at work.
>>
>> However, the latch is a bit low, and the dog has figured out how to get
>> out on his own, because he's smart and he's a dog, not a cat.
>> The cat would just climb the tree next to the fence.
>>
>> Helpful Neighbor, bless his heart, decides to solve this problem while
>> you're at work. He welds the gate shut. Problem solved! The dog can't
>> escape anymore.
>>
>> Of course, now *you* can't get into your own backyard either. But hey,
>> the dog's contained.
>>
>> When you point this out, Helpful Neighbor offers solutions: you could
>> climb over the fence (inconvenient), or you could install a new gate that
>> only opens with a 47-step authentication process (safe but unusable), or
>> you could just accept that the backyard is now dog-only territory (give up).
>>
>> What you *actually* needed was a latch that works for humans but not
>> dogs. A solution that achieves the safety goal without sacrificing basic
>> functionality.
>>
>> C++20 had a Helpful Neighbor moment with `std::atomic`.
>>
>> # Motivation
>>
>> ## The Trilemma
>>
>> Consider a type author who wants to create a type that:
>>
>> 1. **Has safe default initialization** — objects are never left
>> uninitialized
>> 2. **Is an implicit lifetime type** — can be used in shared memory,
>> memory-mapped files, or with `std::start_lifetime_as`
>> 3. **Cannot be copied or moved** — the type semantically represents a
>> unique resource
>>
>> Today, it's near impossible.
>>
>> To be an implicit lifetime type, a class must, among other things, have
>> at least one trivial constructor. The only way to explicitly specify a
>> trivial constructor is via `= default`. But you can't only use `= default`
>> user-defined constructors. Hence, no way for anything but the default,
>> copy, and move constructors to be trivial.
>>
>> If you delete copy and move (requirement 3), only the default constructor
>> remains eligible to be trivial. But if you make the default constructor
>> trivial (requirement 2), objects can be left uninitialized (violating
>> requirement 1).
>>
>> Pick any two. You can't have all three.
>>
>> | Choice | What You Get | What You Lose |
>> |--------|--------------|---------------|
>> | Safe default ctor | Objects always initialized | Not an implicit
>> lifetime type |
>> | Trivial default ctor | Implicit lifetime type | Uninitialized objects
>> possible |
>> | No default ctor | No uninitialized objects | No `Foo x;` or `Foo x{};`
>> syntax |
>>
>> This isn't a theoretical concern. It's the exact situation `std::atomic`
>> finds itself in after C++20.
>>
>> ## Real-World Impact
>>
>> The implicit lifetime type requirement isn't academic. It's essential for:
>>
>> - **Shared memory** — inter-process communication via memory-mapped
>> regions
>> - **Memory-mapped files** — persistent data structures
>> - **Custom allocators** — placement new into raw storage
>> - **`std::start_lifetime_as`** — explicit lifetime management
>>
>> Every C++ message queue library uses atomics in shared memory. Lock-free
>> data structures in shared memory are a common pattern. All of this code is
>> now, technically speaking, undefined behavior.
>>
>> The standard even explicitly contemplates cross-process atomics.
>> [atomics.lockfree] discusses lock-free atomics working correctly: "This
>> restriction enables communication by memory that is mapped into a process
>> more than once and by memory that is shared between two processes."
>> Except now, getting a `std::atomic` *into* shared memory with defined
>> behavior is impossible.
>>
>> "But it works!" you might say. And you'd be right — today. Compilers are
>> getting smarter. Optimizers are getting more aggressive. Undefined behavior
>> that "works" today may not work tomorrow. We've all seen this movie before.
>>
>> # The `std::atomic` Story
>>
>> ## What Happened
>>
>> In C++20, P0883R2 "Fixing Atomic Initialization" changed `std::atomic`'s
>> default constructor. Previously, it was trivial (did nothing). After P0883,
>> it explicitly zero-initializes.
>>
>> The motivation was reasonable: users writing `std::atomic<int> counter;`
>> would get an uninitialized atomic, which is almost certainly a bug. Making
>> the default constructor zero-initialize catches this class of errors.
>>
>> But, in that same C++20 cycle, P0593R6 "Implicit creation of objects for
>> low-level object manipulation" formalized implicit lifetime types,
>> including the part about trivial constructors.
>>
>> The result? C++20 simultaneously:
>>
>> 1. Defined what implicit lifetime types are
>> 2. Made `std::atomic` not be one
>>
>> ## The Constructor Situation
>>
>> Let's look at `std::atomic`'s constructors prior to C++20:
>>
>> | Constructor | Status | Trivial? |
>> |-------------|--------|----------|
>> | Default | = default | Yes, if T is trivial |
>> | Copy | Deleted | N/A |
>> | Move | Deleted | N/A |
>> | Value | Takes a T | No (user-defined) |
>>
>> This type is an implicit lifetime type, according to the definition
>> drafted for C++20.
>>
>> And the is `std::atomic`'s constructors after C++20:
>>
>> | Constructor | Status | Trivial? |
>> |-------------|--------|----------|
>> | Default | Zero-initializes | No |
>> | Copy | Deleted | N/A |
>> | Move | Deleted | N/A |
>> | Value | Takes a T | No (user-defined) |
>>
>> Zero trivial constructors. Not an implicit lifetime type. No way to use
>> it in shared memory with defined behavior.
>>
>> The P0883 authors had good intentions. They saw a safety problem and
>> fixed it. But like our Helpful Neighbor, they welded the gate shut in the
>> process.
>>
>> ## Why Not Just Undo It?
>>
>> We could revert P0883's change to the default constructor. But that
>> brings back uninitialized atomics, which genuinely is a footgun.
>>
>> We could make the default constructor private (and trivial).
>> This would satisfy implicit lifetime requirements because there is a
>> trivial default constructor, even though it is private.
>> But, this would prevent accidentally creating an uninitialized instance
>> since `std::atomic<int> x;` would no longer compile.
>> Unfortunately, neither would `std::atomic<int> x;{}`.
>>
>> While a compile error is better than silent uninitialization, it's a
>> breaking change, and it's still inconvenient.
>>
>> What we need is a way to have *both*: a safe default constructor *and* a
>> trivial constructor for implicit lifetime purposes. The language currently
>> doesn't allow this.
>>
>> # Proposed Solution
>>
>> ## User-Defined Trivial Constructors
>>
>> We propose allowing `= default` on constructors that are not special
>> member functions:
>>
>> ```cpp
>> struct Foo {
>> int value_;
>>
>> // Safe: always initializes
>> Foo() : value_{0} {}
>>
>> // Trivial: enables implicit lifetime
>> explicit Foo(std::trivial_t) = default;
>> };
>> ```
>>
>> The semantics are straightforward:
>>
>> - `= default` on a user-defined constructor means "do nothing"
>> - It is trivial — exactly what `Foo() = default` would do
>> - The parameter exists only for overload resolution
>> - It is only valid if `Foo() = default` would produce a trivial default
>> constructor
>> - It must be marked `explicit` to prevent unintended use and because
>> `explicit` should be the default, but that's not a nice story about my dog
>> and my neighbor.
>>
>> ## Usage
>>
>> ```cpp
>> // Normal usage — safe, always initialized
>> Foo regular;
>> Foo also_regular{};
>>
>> // Explicit trivial construction — for shared memory scenarios
>> // where the object is constructed and manually initialized later.
>> Foo in_shared_memory(std::trivial);
>> ```
>>
>> In practice, the trivial constructor may never be called directly. Its
>> existence is what matters — it makes the type an implicit lifetime type,
>> enabling `std::start_lifetime_as`, `mmap`, and similar operations to
>> implicitly begin the object's lifetime.
>>
>> ## Impact on `std::atomic`
>>
>> With this feature, `std::atomic` can be fixed with zero breaking changes:
>>
>> ```cpp
>> template<typename T>
>> struct atomic {
>> // ... existing members ...
>>
>> // Existing: safe zero-initializing default ctor
>> atomic() noexcept : value_{} {}
>>
>> // New: trivial ctor for implicit lifetime
>> explicit atomic(std::trivial_t) = default;
>> };
>> ```
>>
>>
>> Existing code continues to work exactly as before. The only change is
>> that `std::atomic<int>` is now, once again, an implicit lifetime type.
>>
>> # Alternative Solutions Considered
>>
>> There are numerous options for addressing the `std::atomic` problem, as
>> summarized below.
>>
>> These are, it happens, the same options users have for their own types,
>> many of which have been in use for decades.
>>
>> For example, the Boost.Interprocess types all fall into this same
>> category.
>> They are used within shared memory, and some require explicit
>> construction.
>> They sit on top of native builtin types that could be implicitly created,
>> but there is no explicit way to specify this.
>>
>>
>> ## Committee Fiat
>>
>> The committee could simply declare that `std::atomic<T>` is an implicit
>> lifetime type for lock-free built-in types, rules be damned.
>>
>> This works for `std::atomic` but sets a troubling precedent.
>> It doesn't help user-defined types facing the same trilemma.
>> And "we'll just special-case it" isn't a scalable design philosophy.
>>
>> ## New Atomic Type
>>
>> We could introduce a new type, `std::basic_atomic` or similar, that
>> maintains trivial constructors.
>>
>> This splits the ecosystem. Library authors would need to decide which
>> atomic to use. Existing code using `std::atomic` in shared memory remains
>> broken. And again, it doesn't solve the general problem.
>>
>> ## `std::atomic_ref`
>>
>> "Just use `std::atomic_ref` with raw integer types in shared memory."
>>
>> This is viable, but:
>>
>> - Existing code is still undefined behavior
>> - It requires discipline: *all* accesses must go through `atomic_ref`
>> - The standard explicitly states: "While any `atomic_ref` instances exist
>> that reference the `*ptr` object, all accesses to that object shall
>> exclusively occur through those `atomic_ref` instances"
>> - One direct access without the wrapper is undefined behavior
>> - This is easy to get wrong
>>
>> ## Private Trivial Default Constructor
>>
>> ```cpp
>> template<typename T>
>> struct atomic {
>> private:
>> atomic() = default; // Trivial, satisfies implicit lifetime
>> public:
>> // Users must explicitly initialize
>> atomic(T desired) noexcept : value_(desired) {}
>> };
>> ```
>>
>> This achieves implicit lifetime status and prevents uninitialized atomics
>> (the old `std::atomic<int> x;` becomes a compile error). But it's a
>> breaking change — existing code using `std::atomic<int> x{};` would fail.
>>
>> Without a language change, this is the best we can get.
>> It's inconvenient for all, and requires lots of code change to adopt, but
>> it works.
>>
>> ## Why User-Defined Trivial Constructors Win
>>
>> The proposed solution:
>>
>> - **Solves the general problem** — any type can benefit, not just
>> `std::atomic`
>> - **Is non-breaking** — existing code continues to work
>> - **Is principled** — no special-case magic, just a natural extension of
>> `= default`
>> - **Achieves both goals** — safe default initialization AND implicit
>> lifetime eligibility
>>
>> # Design Decisions
>>
>> ## Tag Type
>>
>> The proposal uses a standard-provided tag type `std::trivial_t` (with a
>> corresponding `std::trivial` value).
>> This ensures uniform usage across the ecosystem.
>> I'm not in love with the name, and would happily consider alternatives.
>>
>> An alternative design would allow *any* constructor signature to use `=
>> default`.
>> This provides maximum flexibility because we don't know what the future
>> holds, but may be more than necessary.
>> The tag type approach is simpler to specify and makes intent clear.
>>
>> We're open to either approach — this is an area where committee feedback
>> would be valuable.
>>
>> ## Why `explicit`?
>>
>> The trivial constructor should be marked `explicit` to prevent accidental
>> use:
>>
>> ```cpp
>> void takes_foo(Foo);
>>
>> takes_foo({}); // Uses default ctor — safe
>> takes_foo(std::trivial); // Compile error — explicit required
>> takes_foo(Foo(std::trivial)); // OK — intentional
>> ```
>>
>> This ensures developers don't accidentally create uninitialized objects
>> through implicit conversions.
>>
>> Not to mention, that `explicit` should be the default for constructors,
>> and you must say `explicit(false)` to get an implicit one, but that doesn't
>> fit with a cozy story about my dog and my neighbor.
>>
>> ## Constraint: Must Be Trivial
>>
>> The feature is only valid when the equivalent `Foo() = default` would be
>> trivial.
>> If a class has members with non-trivial default constructors,
>> `Foo(std::trivial_t) = default` is ill-formed.
>>
>> This prevents misuse — you can't create a "trivial" constructor that
>> actually isn't.
>>
>> # Implementation Experience
>>
>> [TODO: Seek implementation experience from compiler vendors]
>>
>> # Wording
>>
>> [TODO: Detailed wording changes. Key areas:]
>>
>> - Extend [class.default.ctor] or create new section for defaulted
>> non-special-member constructors
>> - Define when such a constructor is trivial
>> - Add `std::trivial_t` to `<type_traits>` or a new header
>> - Specify `is_trivially_constructible` behavior
>> - Update `std::atomic` specification
>> - Others???
>>
>> # Acknowledgments
>>
>> [TODO]
>>
>> # References
>>
>> - P0883R2 "Fixing Atomic Initialization"
>> - P0593R6 "Implicit creation of objects for low-level object manipulation"
>> - [basic.life] Object lifetime
>> - [class.default.ctor] Default constructors
>> - [atomics.lockfree] Lock-free property
>> --
>> Std-Proposals mailing list
>> Std-Proposals_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>
>
Received on 2026-01-10 16:08:54
