Placement New and Assignment of Class with Const Member

Placement new and assignment of class with const member

There is nothing that makes the shown code snippet inherently UB. However, it is almost certain UB will follow immediately under any normal usage.

From [basic.life]/8 (emphasis mine)

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:

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

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

  • 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

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

Since there is a const member in s, using the original variable after a call to operator= will be UB.

s var{42};
var = s{420}; // OK
do_something(var.id); // UB! Reuses s through original name
do_something(std::launder(&var)->id); // OK, this is what launder is used for

Move construction and assignment of class with constant member

Unfortunately this is undefined behavior. You cannot overwrite a const object like this and refer to it by the same name afterwards. This is covered by [basic.life]/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: [...]

  • 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, [...]

The simple fix is to make i non const and private and just use a getter to stop any modification from happening.

assignment of class with const member

C++03 requires that elements stored in containers be CopyConstructible and Assignable (see §23.1). So implementations can decide to use copy construction and assignment as they see fit. These constraints are relaxed in C++11. Explicitly, the push_back operation requirement is that the type be CopyInsertable into the vector (see §23.2.3 Sequence Containers)

Furthermore, C++11 containers can use move semantics in insertion operations and do on.

Replace a variable of a type with const members

const members are problematic in general for the very reason you discovered.

The much simpler alternative is to make the member private and take care to provide no means to modify it from outside the class:

class MyClass {
public:
MyClass(int a) : a(a) {
}
MyClass() : MyClass(0) {
}
~MyClass() {
}
private:
int a;
};

I did not add a getter yet, because you say access via myObject.a is a hard requirement. Enabling this requires a bit of boilerplate, but it is much less hacky than modifiying something that must not be modified:

class MyClass {
public:
struct property {
const int& value;
operator int(){ return value;}
property(const property&) = delete;
};

MyClass(int a = 0) : value(a) {}
private:
int value;
public:
property a{value};
};

int main(){
MyClass myObject{5};
int x = myObject.a;
//myObject.a = 42; // error
//auto y = myObject.a; // unexpected type :/
}

Live Demo

Drawback is that it does not play well with auto. If by any means you can accept myObject.a() I would suggest to use that and keep it simple.

Is using a placement new as a copy assignment operator bad?

is there any really big reason why this shouldn't be done?

  1. You must be absolutely sure that every derived class defines its own assignment operator, even if it is trivial. Because an implicitly defined copy-assignment operator of a derived class will screw everything. It'll call S::operator= which will re-create a wrong type of object in its place.

  2. Such destroy-and-construct assignment operator can't be re-used by any derived class. So, not only you are forcing derived classes to provide an explicit copy operator, but you're forcing them to stick to the same destroy-and-construct idiom in their assignment operator.

  3. You must be absolutely sure that no other thread is accessing the object while it is being destroyed-and-constructed by such assignment operator.

  4. A class may have some data members that must not be affected by the assignment operator. For example, a thread-safe class may have some kind of mutex or critical section member, with some other thread waiting on them right when the current thread is going to destroy-and-construct that mutex...

  5. Performance-wise, it has virtually no advantage over standard copy-and-swap idiom. So what would be the gain in going through all the pain mentioned above?

const member and assignment operator. How to avoid the undefined behavior?

Your code causes undefined behavior.

Not just "undefined if A is used as a base class and this, that or the other". Actually undefined, always. return *this is already UB, because this is not guaranteed to refer to the new object.

Specifically, consider 3.8/7:

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:

...

— 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,

Now, "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" is exactly what you are doing.

Your object is of class type, and it does contain a non-static data member whose type is const-qualified. Therefore, after your assignment operator has run, pointers, references and names referring to the old object are not guaranteed to refer to the new object and to be usable to manipulate it.

As a concrete example of what might go wrong, consider:

A x(1);
B y(2);
std::cout << x.c << "\n";
x = y;
std::cout << x.c << "\n";

Expect this output?

1
2

Wrong! It's plausible you might get that output, but the reason const members are an exception to the rule stated in 3.8/7, is so that the compiler can treat x.c as the const object that it claims to be. In other words, the compiler is allowed to treat this code as if it was:

A x(1);
B y(2);
int tmp = x.c
std::cout << tmp << "\n";
x = y;
std::cout << tmp << "\n";

Because (informally) const objects do not change their values. The potential value of this guarantee when optimizing code involving const objects should be obvious. For there to be any way to modify x.c without invoking UB, this guarantee would have to be removed. So, as long as the standard writers have done their job without errors, there is no way to do what you want.

[*] In fact I have my doubts about using this as the argument to placement new - possibly you should have copied it to a void* first, and used that. But I'm not bothered whether that specifically is UB, since it wouldn't save the function as a whole.

can placement new be used to alter const data?

You can reuse the storage after the lifetime of the object has ended. The lifetime ends with the destructor call. There's nothing technically problematic in that.

Using the object after its lifetime has ended, as the presented example code did at the time I wrote this answer in

pt.~point2d();
new (&pt) point2d { pt.x + value, pt.y };

is Undefined Behavior.

If you insist on using the point class with const fields, you can work around that like this:

void move_x( int const value )
{
auto const old_pt = pt;
pt.~point2d();
::new (&pt) point2d { old_pt.x + value, old_pt.y };
}

That may feel like unnecessary complication and possible micro-inefficiency, but rather, the unnecessary complication is the point class.

move assignment to object with const value

I would suggest moving to std::unique_ptr:

void func() {
static std::unique_ptr<OBJ> obj = std::make_unique<OBJ>();
std::unique_ptr<OBJ> other = std::make_unique<OBJ>(); // random values
if(condition)
obj = std::move(other); //move
}

This should be your choice in many cases where there is a need to move something that cannot be moved, to hold an unknown polymorphic type or any other case where you cannot deal with the actual type.

placement new on a class with reference field

To answer the currently open questions:

First question:

  • What is the case of such code in C++14 or before (when std::launder didn't exist)? Probably it is UB - this is why std::launder was brought to the game, right?

Yes, it was UB. This is mentioned explicitly in the NB issues @Language Lawyer referred to:

Because of that issue all the standard libraries have undefined behaviors in widely used types. The only way to fix that issue is to adjust the lifetime rules to auto-launder the placement new.
(https://github.com/cplusplus/nbballot/issues/7)

Second question:

If in C++20 we do not need std::launder for such a case, how the compiler can understand that the reference is being manipulated without our help (i.e. without "Pointer optimization barrier") to avoid caching of the reference value?

Compilers already know to not optimize object (or sub-object) value this way if a non-const member function was called between two usages of the object or if any function was called with the object as a parameter (passed by-ref), because this value may be changed by those functions. This change to the standard just added a few more cases where such optimization is illegal.



Related Topics



Leave a reply



Submit