Const Member and Assignment Operator. How to Avoid the Undefined Behavior

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.

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.

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

Operator = Overload with Const Variable in C++

You are almost there. Few noteworthy points:

  • The name should not be const qualified. A const cannot be modified, which is exactly what we want in the assignment operator.

  • The C++ keyword is class and not Class as your code has it (it'll give you compile errors)

  • As Michael Burr notes: "It should be noted though that if the class simply contains other classes that already properly support assignment (as in this case with a simple string member), the implicit, compiler-generated operator=() will work just fine." Here, in your case, the only member string has a proper op=. So explicitly defining is redundant.

  • Meeh's solution is almost there. The only thing it doesn't talk about is self-assignment. Read FAQ 12.

  • Assignment is one the Big Three member functions FAQ 27.10. Look it up. It says, requirement to implement either one of copy ctor, op= or the dtor usually implies that you'd need to implement the other two as well.

The corrected code sample should be something like this:

class Doctor {
string name;
public:
Doctor& operator=(Doctor const& o) {
if (&o != this) name = o.name;
return *this;
}
// ...
};

Is swapping a const member undefined behavior? C++17

This seems to be an area of c++ evolution.

It's UB in c++17 because of this restriction on replacing const member objects

However, in later versions, this restriction is mostly removed.. There remains a restriction on complete const objects. For instance you can't do this if MyInt i0(0); was const MyInt i0(0);

Even though c++20 now allows modification of const sub objects, it's best to avoid const_cast and use this to create an assignment ctor. In this case one doesn't need a destructor since it's trivially destructable. Note that one had to use placement new prior to c++20 and that was still UB since const sub objects were not permitted to change previously.

constexpr MyInt& operator=(MyInt&& rh)
{
std::construct_at(&this->i, rh.i);
return *this;
}

C++: STL troubles with const class members

As AndreyT pointed out, under these circumstances assignment (mostly) doesn't make a lot of sense. The problem is that vector (for one example) is kind of an exception to that rule.

Logically, you copy an object into the vector, and sometime later you get back another copy of the original object. From a purely logical viewpoint, there's no assignment involved. The problem is that vector requires that the object be assignable anyway (actually, all C++ containers do). It's basically making an implementation detail (that somewhere in its code, it might assign the objects instead of copying them) part of the interface.

There is no simple cure for this. Even defining your own assignment operator and using const_cast doesn't really fix the problem. It's perfectly safe to use const_cast when you get a const pointer or reference to an object that you know isn't actually defined to be const. In this case, however, the variable itself is defined to be const -- attempting to cast away the constness and assign to it gives undefined behavior. In reality, it'll almost always work anyway (as long as it's not static const with an initializer that's known at compile time), but there's no guarantee of it.

C++ 11 and newer add a few new twists to this situation. In particular, objects no longer need to be assignable to be stored in a vector (or other collections). It's sufficient that they be movable. That doesn't help in this particular case (it's no easier to move a const object than it is to assign it) but does make life substantially easier in some other cases (i.e., there are certainly types that are movable but not assignable/copyable).

In this case, you could use a move rather than a copy by adding a level of indirection. If your create an "outer" and an "inner" object, with the const member in the inner object, and the outer object just containing a pointer to the inner:

struct outer { 
struct inner {
const double coeff;
};

inner *i;
};

...then when we create an instance of outer, we define an inner object to hold the const data. When we need to do an assignment, we do a typical move assignment: copy the pointer from the old object to the new one, and (probably) set the pointer in the old object to a nullptr, so when it's destroyed, it won't try to destroy the inner object.

If you wanted to badly enough, you could use (sort of) the same technique in older versions of C++. You'd still use the outer/inner classes, but each assignment would allocate a whole new inner object, or you'd use something like a shared_ptr to let the outer instances share access to a single inner object, and clean it up when the last outer object is destroyed.

It doesn't make any real difference, but at least for the assignment used in managing a vector, you'd only have two references to an inner while the vector was resizing itself (resizing is why a vector requires assignable to start with).

Non-static const member, can't use default assignment operator

You have a case like this:

struct sample {
int const a; // const!

sample(int a):a(a) { }
};

Now, you use that in some context that requires sample to be assignable - possible in a container (like a map, vector or something else). This will fail, because the implicitly defined copy assignment operator does something along this line:

// pseudo code, for illustration
a = other.a;

But a is const!. You have to make it non-const. It doesn't hurt because as long as you don't change it, it's still logically const :) You could fix the problem by introducing a suitable operator= too, making the compiler not define one implicitly. But that's bad because you will not be able to change your const member. Thus, having an operator=, but still not assignable! (because the copy and the assigned value are not identical!):

struct sample {
int const a; // const!

sample(int a):a(a) { }

// bad!
sample & operator=(sample const&) { }
};

However in your case, the apparent problem apparently lies within std::pair<A, B>. Remember that a std::map is sorted on the keys it contains. Because of that, you cannot change its keys, because that could easily render the state of a map invalid. Because of that, the following holds:

typedef std::map<A, B> map;
map::value_type <=> std::pair<A const, B>

That is, it forbids changing its keys that it contains! So if you do

*mymap.begin() = make_pair(anotherKey, anotherValue);

The map throws an error at you, because in the pair of some value stored in the map, the ::first member has a const qualified type!

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.

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.



Related Topics



Leave a reply



Submit