C++ Logo

std-proposals

Advanced search

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

From: Jody Hagins <coachhagins_at_[hidden]>
Date: Sat, 10 Jan 2026 12:30:59 -0500
Hmmm. I should have read this before spending all that time on my long-winded response.

I could have just said, "What Breno said."



> On Jan 10, 2026, at 10:07 AM, Breno Guimarães via Std-Proposals <std-proposals_at_[hidden]> wrote:
>
> If you have a type that has a trivial constructor and a non-trivial constructor you can get to a new object through the two routes: calling the non-trivial constructor or calling (or skipping) the trivial constructor.
>
> He is proposing a trivial constructor to be added, but one that takes an argument so it's not going to be the default one.
>
> In the shared memory context, the "start lifetime as" makes sense because the object could have been there through the trivial constructor.
> And if code is actually going to explicitly call the constructor they use the non-trivial one.
>
> Makes perfect sense to me.
>
> I enjoyed reading the paper. I recommend reading it.
>
> Breno G.
>
> Em sáb., 10 de jan. de 2026, 11:28, Peter Bindels via Std-Proposals <std-proposals_at_[hidden] <mailto:std-proposals_at_[hidden]>> escreveu:
>> I've tried to read the paper but in my mind it seems to try to solve an impossible problem.
>>
>> Trivial lifetime objects are those where the constructor can be skipped. Guaranteed initialization means the constructor can never be skipped.
>>
>> You want to have a type that is guaranteed initialized (so always running a constructor), but that's trivial lifetime (so it can be skipped) ?
>>
>> Can you give an example of what should happen if you have a std::atomic<int> in shared memory somewhere, and two programs with memory access try to create that on the same memory area? What do you want to happen, why should that happen?
>>
>> Thanks,
>> Peter
>>
>> On Sat, Jan 10, 2026 at 3:09 PM Jody Hagins via Std-Proposals <std-proposals_at_[hidden] <mailto: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] <mailto: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] <mailto:Std-Proposals_at_[hidden]>
>>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>> --
>> Std-Proposals mailing list
>> Std-Proposals_at_[hidden] <mailto:Std-Proposals_at_[hidden]>
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals


Received on 2026-01-10 17:31:14