Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I answered the question about std::vector of objects and const-correctness, and received a comment about undefined behavior. I do not agree and therefore I have a question.

Consider the class with const member:

class A {  public:      const int c; // must not be modified!      A(int c) : c(c) {}      A(const A& copy) : c(copy.c) { }          // No assignment operator };  

I want to have an assignment operator but I do not want to use const_cast like in the following code from one of the answers:

A& operator=(const A& assign)  {      *const_cast<int*> (&c)= assign.c;  // very very bad, IMHO, it is undefined behavior     return *this;  }  

My solution is

// Custom-defined assignment operator A& operator=(const A& right)   {       if (this == &right) return *this;        // manually call the destructor of the old left-side object     // (`this`) in the assignment operation to clean it up     this->~A();      // use "placement new" syntax to copy-construct a new `A`      // object from `right` into left (at address `this`)     new (this) A(right);      return *this;   }   

Do I have undefined behavior (UB)?

What would be a solution without UB?

like image 374
Alexey Malistov Avatar asked Nov 09 '10 16:11

Alexey Malistov


People also ask

Is Const cast undefined behavior?

Even though const_cast may remove constness or volatility from any pointer or reference, using the resulting pointer or reference to write to an object that was declared const or to access an object that was declared volatile invokes undefined behavior. So yes, modifying constant variables is undefined behavior.

What happens if you dont define a copy assignment operator?

If no user-defined copy assignment operators are provided for a class type (struct, class, or union), the compiler will always declare one as an inline public member of the class.

How to define a copy assignment operator in c++?

The implicitly declared copy assignment operator of a class A will have the form A& A::operator=(const A&) if the following statements are true: A direct or virtual base B of class A has a copy assignment operator whose parameter is of type const B& , const volatile B& , or B .

Is assignment operator automatically generated?

It is well-known what the automatically-generated assignment operator will do - that's defined as part of the standard and a standards-compliant C++ compiler will always generate a correctly-behaving assignment operator (if it didn't, then it would not be a standards-compliant compiler).


1 Answers

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.

like image 71
Steve Jessop Avatar answered Sep 23 '22 03:09

Steve Jessop