c++ - Any way to have an Implicit Lifetime Type with a user defined constructor? - Stack Overflow

admin2025-04-17  2

TLDR;

Can we make a non-copyable, non-movable class with zero default initialization into an implicit lifetime type without changing the existing constructors?


Directly from the C++23 specification, 11.2

   9  A class S is an implicit-lifetime class if
(9.1)     — it is an aggregate whose destructor is not user-provided or
(9.2)     — it has at least one trivial eligible constructor and a trivial, non-deleted destructor.

But what does "at least one" mean? Sounds like any one of the many possible constructors. However, the spec also says that for a constructor to be trivial, it must be...

   Implicitly declared or explicitly defaulted (= default) on its first declaration.

Since only special member functions can be either of those, only special member functions can technically be trivial. So, the answer, in my mind: not a chance.

But I ain't the sharpest tool in the shed and may be missing something in the standard.


Consider this minimized existing class.

template <typename T>
struct Foo
{
    T t;

    Foo() : t() { }
    Foo(Foo const &) = delete;
    Foo(Foo &&) = delete;
};

static_assert(not std::is_trivially_default_constructible_v<Foo<int>>);
static_assert(not std::is_trivially_copy_constructible_v<Foo<int>>);
static_assert(not std::is_trivially_move_constructible_v<Foo<int>>);

But, we want to be able to use instances of this type in situations that require it to be an implicit lifetime type (when T could be used in such a way). It is not currently an implicit lifetime type.

So, we add this.

template <typename T>
struct Foo
{
    // ...
    Foo(tag_t) { } // Kneel, bow, and pray...
};

Effectively, Foo<int>::foo(tag_t) behaves as a trivial constructor (same for Foo() {} ).

Technically, Foo<int>::Foo(tag_t) is not a trivial constructor (same for Foo() {} ).

Furthermore, the reference implementation of std::is_implicit_lifetime in P2674 only checks the default/copy/move constructors for triviality. I get that because anything else would require compiler support. However, the standard itself says any constructor, and the compiler can see that this constructor does nothing, so it could detect that there is an effective trivial constructor... and I'm crossing my fingers and my toes.

I'm guessing it doesn't, but as of today, no official releases of any major vendor provide this type trait.

So, given that the default/copy/move constructors are not trivial - how do you make Foo into an implicit lifetime type - without changing the current default/copy/move constructor - that would break any current code that uses those types?


Before you tell me not to write classes like that, this isn't one of my classes. It's for, um, yeah, it's for a friend. See, there is already at least one just like that in the standard library. One that is being treated as if it were an implicit lifetime type in lots of existing code. But, technically, it is not, and I'm trying to make it one without having to write a paper.


EDIT

I was trying not to get into issues about specific types here but to keep it to the standard to see if there is a way around this issue. However, kind people who want to help ask the same question in different ways, so here goes (and this is almost as short as I can keep it).

In C++20, std::atomic was modified to enforce zero-initialization through its default constructor. While this change improved safety for most use cases, it inadvertently made std::atomic unsuitable for shared memory scenarios by removing its trivial default constructor and implicit lifetime characteristics.

Using std::atomic<int> in shared memory now results in undefined behavior. While std::atomic_ref provides a workaround, it sacrifices encapsulation and type safety. The standard currently offers no type designed explicitly for atomic operations in shared memory despite acknowledging this use case in its specification of lock-free operations.

IMO, this was an oversight at the time. The paper, P0883, was trying to make sure that std::atomic<int> i{} was zero initialized. Yeah, that looks strange - please read the paper if you'd like more details. It went further than that, though, and made default initialization of all std::atomic types zero initialize instead of default initialized.

So, since the beginning, the copy/move constructors of std::atomic have been deleted, for good reason. With C++20, the default constructor is no longer trivial, even when T is int.

This is the exact scenario as my Foo example. I didn't think it mattered whether we were talking about my Foo or std::mutex, and bringing std::mutex into it just makes the discussion become more complicated.

TLDR;

Can we make a non-copyable, non-movable class with zero default initialization into an implicit lifetime type without changing the existing constructors?


Directly from the C++23 specification, 11.2

   9  A class S is an implicit-lifetime class if
(9.1)     — it is an aggregate whose destructor is not user-provided or
(9.2)     — it has at least one trivial eligible constructor and a trivial, non-deleted destructor.

But what does "at least one" mean? Sounds like any one of the many possible constructors. However, the spec also says that for a constructor to be trivial, it must be...

   Implicitly declared or explicitly defaulted (= default) on its first declaration.

Since only special member functions can be either of those, only special member functions can technically be trivial. So, the answer, in my mind: not a chance.

But I ain't the sharpest tool in the shed and may be missing something in the standard.


Consider this minimized existing class.

template <typename T>
struct Foo
{
    T t;

    Foo() : t() { }
    Foo(Foo const &) = delete;
    Foo(Foo &&) = delete;
};

static_assert(not std::is_trivially_default_constructible_v<Foo<int>>);
static_assert(not std::is_trivially_copy_constructible_v<Foo<int>>);
static_assert(not std::is_trivially_move_constructible_v<Foo<int>>);

But, we want to be able to use instances of this type in situations that require it to be an implicit lifetime type (when T could be used in such a way). It is not currently an implicit lifetime type.

So, we add this.

template <typename T>
struct Foo
{
    // ...
    Foo(tag_t) { } // Kneel, bow, and pray...
};

Effectively, Foo<int>::foo(tag_t) behaves as a trivial constructor (same for Foo() {} ).

Technically, Foo<int>::Foo(tag_t) is not a trivial constructor (same for Foo() {} ).

Furthermore, the reference implementation of std::is_implicit_lifetime in P2674 only checks the default/copy/move constructors for triviality. I get that because anything else would require compiler support. However, the standard itself says any constructor, and the compiler can see that this constructor does nothing, so it could detect that there is an effective trivial constructor... and I'm crossing my fingers and my toes.

I'm guessing it doesn't, but as of today, no official releases of any major vendor provide this type trait.

So, given that the default/copy/move constructors are not trivial - how do you make Foo into an implicit lifetime type - without changing the current default/copy/move constructor - that would break any current code that uses those types?


Before you tell me not to write classes like that, this isn't one of my classes. It's for, um, yeah, it's for a friend. See, there is already at least one just like that in the standard library. One that is being treated as if it were an implicit lifetime type in lots of existing code. But, technically, it is not, and I'm trying to make it one without having to write a paper.


EDIT

I was trying not to get into issues about specific types here but to keep it to the standard to see if there is a way around this issue. However, kind people who want to help ask the same question in different ways, so here goes (and this is almost as short as I can keep it).

In C++20, std::atomic was modified to enforce zero-initialization through its default constructor. While this change improved safety for most use cases, it inadvertently made std::atomic unsuitable for shared memory scenarios by removing its trivial default constructor and implicit lifetime characteristics.

Using std::atomic<int> in shared memory now results in undefined behavior. While std::atomic_ref provides a workaround, it sacrifices encapsulation and type safety. The standard currently offers no type designed explicitly for atomic operations in shared memory despite acknowledging this use case in its specification of lock-free operations.

IMO, this was an oversight at the time. The paper, P0883, was trying to make sure that std::atomic<int> i{} was zero initialized. Yeah, that looks strange - please read the paper if you'd like more details. It went further than that, though, and made default initialization of all std::atomic types zero initialize instead of default initialized.

So, since the beginning, the copy/move constructors of std::atomic have been deleted, for good reason. With C++20, the default constructor is no longer trivial, even when T is int.

This is the exact scenario as my Foo example. I didn't think it mattered whether we were talking about my Foo or std::mutex, and bringing std::mutex into it just makes the discussion become more complicated.

Share Improve this question edited Feb 3 at 5:45 Patrick Roberts 52.1k10 gold badges117 silver badges163 bronze badges asked Feb 1 at 0:38 Jody HaginsJody Hagins 28.4k6 gold badges60 silver badges93 bronze badges 6
  • 2 Why do you want to allow evading the invariant of the class that its members are initialized? What standard-library type are we talking about? – Davis Herring Commented Feb 1 at 2:28
  • Informally "Trivial" means equivalent of noop/memcpy/memmove. Any function with a body is potentially more than noop; Specifically, a constructor not defined as =default will initialize its sub-objects to zero. That means any not-defaulted constructor is not trivial. But outsides the special memebers default definition doesn't make sence; So other constructors are automatically overruled from being trivial - due to the fact that they cannot have a default definition. – Red.Wave Commented Feb 1 at 7:50
  • Do you realize you are making a contradictory requirement statement here? It is not only about Foo but also about T. T must be trivially copyable/constructible/destructible and Foo should be constrained to that (concepts). Tell your friend that fallout you get after that adding that requirement/constraint to Foo is code that needs fixing. – Pepijn Kramer Commented Feb 1 at 7:58
  • I think all of you are asking the same question, just in a different way. I tried to avoid getting into too many issues, but I edited the question to hopefully address your questions - and keep others from being confused and asking similar questions. – Jody Hagins Commented Feb 1 at 9:34
  • 1 @PatrickRoberts—I Agree. The paper acknowledges that compilers didn't implement it that way, and I believe the standard didn't dictate that anyway (the wording was possibly ambiguous). I think this was an overreach and should have been left trivial for trivial types. However, once it was made that way, it can't be changed back without breaking code that now assumes default initialization will zero-initialize. The only option to prevent UB is to use a native type and then use atomic_ref or C/native atomic functions. With the sync functions, atomic is dangerous across processes anyway. – Jody Hagins Commented Feb 3 at 19:18
 |  Show 1 more comment

2 Answers 2

Reset to default 1

Can we make a non-copyable, non-movable class with zero default initialization into an implicit lifetime type without changing the existing constructors?

No. The intent of the standard (and the wording) is fairly clear, it wants a type that can be either constructed uninitialized or by a bitwise copy, and destroying which is a no-op.

Yours can't be constructed this way, so it can't be implicit-lifetime.


But I don't think trying to reach formal correctness in this case is a useful goal.

If std::atomic worked for your purposes before, it won't suddenly stop working in C++20 (assuming you don't have a template that explicitly checks those type requirements).

The standard wording on lifetimes is more strict than what implementations actually enforce in practice (and is wording is slowly getting more and more relaxed).

The standard doesn’t have any concept of shared memory (beyond observing that some implementations of concurrency primitives can support this case by avoiding an address-based table). As such, there’s no way to say what we really want, which is not that the second process can create new objects that start with the value representations of the objects created by the first process (which would… diverge from the originals once they were modified?) but that the second process simply uses the same objects.

If we did have such a notion, it would remove the need for implicit-lifetime status, since the lifetime can be started in the normal fashion (in the first process) and the type can have whatever desirable invariants. As such, we don’t want to weaken the implicit-lifetime requirements (and thereby damage invariants) for the sake of a use case that would profit from it only incompletely in two different ways.

转载请注明原文地址:http://anycun.com/QandA/1744842858a88388.html