Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

I am exploring the possibility of implementing true (partially) immutable data structures in C++. As C++ does not seem to distinguish between a variable and the object that variable stores, the only way to truly replace the object (without assignment operation!) is to use placement new:

auto var = Immutable(state0);
// the following is illegal as it requires assignment to
// an immutable object
var = Immutable(state1);
// however, the following would work as it constructs a new object
// in place of the old one
new (&var) Immutable(state1);

Assuming that there is no non-trivial destructor to run, is this legal in C++ or should I expect undefined behaviour? If its standard-dependant, which is the minimal/maximal standard version where I can expect this to work?

Addendum: since it seems people still read this in 2019, a quick note — this pattern is actually legally possible in modern (post 17) C++ using std::launder().

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
166 views
Welcome To Ask or Share your Answers For Others

1 Answer

What you wrote is technically legal but almost certainly useless.

Suppose

struct Immutable {
  const int x;
  Immutable(int val):x(val) {}
};

for our really simple immutable type.

auto var = Immutable(0);
::new (&var) Immutable(1);

this is perfectly legal.

And useless, because you cannot use var to refer to the state of the Immutable(1) you stored within it after the placement new. Any such access is undefined behavior.

You can do this:

auto var = Immutable(0);
auto* pvar1 = ::new (&var) Immutable(1);

and access to *pvar1 is legal. You can even do:

auto var = Immutable(0);
auto& var1 = *(::new (&var) Immutable(1));

but under no circumstance may you ever refer to var after you placement new'd over it.

Actual const data in C++ is a promise to the compiler that you'll never, ever change the value. This is in comparison to references to const or pointers to const, which is just a suggestion that you won't modify the data.

Members of structures declared const are "actually const". The compiler will presume they are never modified, and won't bother to prove it.

You creating a new instance in the spot where an old one was in effect violates this assumption.

You are permitted to do this, but you cannot use the old names or pointers to refer to it. C++ lets you shoot yourself in the foot. Go right ahead, we dare you.

This is why this technique is legal, but almost completely useless. A good optimizer with static single assignment already knows that you would stop using var at that point, and creating

auto var1 = Immutable(1);

it could very well reuse the storage.


Caling placement new on top of another variable is usually defined behaviour. It is usually a bad idea, and it is fragile.

Doing so ends the lifetime of the old object without calling the destructor. References and pointers to and the name of the old object refer to the new one if some specific assumptions hold (exact same type, no const problems).

Modifying data declared const, or a class containing const fields, results in undefined behaviour at the drop of a pin. This includes ending the lifetime of an automatic storage field declared const and creating a new object at that location. The old names and pointers and references are not safe to use.

[Basic.life 3.8]/8:

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • (8.1) the storage for the new object exactly overlays the storage location which the original object occupied, and

  • (8.2) the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and

  • (8.3) the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and

  • (8.4) the original object was a most derived object (1.8) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

In short, if your immutability is encoded via const members, using the old name or pointers to the old content is undefined behavior.

You may use the return value of placement new to refer to the new object, and nothing else.


Exception possibilities make it extremely difficult to prevent code that exdcutes undefined behaviour or has to summarially exit.

If you want reference semantics, either use a smart pointer to a const object or an optional const object. Both handle object lifetime. The first requires heap allocation but permits move (and possibly shared references), the second permits automatic storage. Both move manual object lifetime management out of business logic. Now, both are nullable, but avoiding that robustly is difficult doing it manually anyhow.

Also consider copy on write pointers that permit logically const data with mutation for efficiency purposes.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...