C++ Logo

std-proposals

Advanced search

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

From: Jonathan Wakely <cxx_at_[hidden]>
Date: Sat, 10 Jan 2026 16:50:00 +0000
On Sat, 10 Jan 2026, 14:09 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.
>

I don't think this section adds much value.



> 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.


"can't use" rather than "can't only use"?


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.
>

They can still use std::atomic_ref on plain integers, right?




> 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:50:20