const member and assignment operator. How to avoid the undefined behavior?
How can you possibly assign to an A if it has a const member? You're trying to accomplish something that's fundamentally impossible. Your solution has no new behaviour over the original, which is not necessarily UB but yours most definitely is.
The simple fact is, you're changing a const member. You either need to un-const your member, or ditch the assignment operator. There is no solution to your problem- it's a total contradiction.
Edit for more clarity:
Const cast does not always introduce undefined behaviour. You, however, most certainly did. Apart from anything else, it is undefined not to call all destructors- and you didn't even call the right one- before you placed into it unless you knew for certain that T is a POD class. In addition, there's owch-time undefined behaviours involved with various forms of inheritance.
You do invoke undefined behaviour, and you can avoid this by not trying to assign to a const object.
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.
First: When you make a data member const
, you're telling the compiler and all the world that this data member never changes. Of course then you cannot assign to it and you certainly must not trick the compiler into accepting code that does so, no matter how clever the trick.
You can either have a const
data member or an assignment operator assigning to all data members. You can't have both.
As for your "solution" to the problem:
I suppose that calling the destructor on an object within a member function invoked for that objects would invoke UB right away. Invoking a constructor on uninitialized raw data to create an object from within a member function that's been invoked for an object that resided where now the constructor is invoked on raw data... also very much sounds like UB to me. (Hell, just spelling this out makes my toenails curl.) And, no, I don't have chapter and verse of the standard for that. I hate reading the standard. I think I can't stand its meter.
However, technicalities aside, I admit that you might get away with your "solution" on just about every platform as long as the code stays as simple as in your example. Still, this doesn't make it a good solution. In fact, I'd argue it's not even an acceptable solution, because IME code never stays as simple as that. Over the years it will get extended, changed, mutated, and twisted and then it will silently fail and require a mind-numbing 36hrs shift of debugging in order to find the problem. I don't know about you, but whenever I find a piece of code like this responsible for 36hrs of debugging fun I want to strangle the miserable dumb-wit who did this to me.
Herb Sutter, in his GotW #23, dissects this idea piece by piece and finally concludes that it "is full of pitfalls, it's often wrong, and it makes life a living hell for the authors of derived classes... never use the trick of implementing copy assignment in terms of copy construction by using an explicit destructor followed by placement new, even though this trick crops up every three months on the newsgroups" (emphasize mine).
If you definitely want to have an immutable (but assignable) member, then without UB you can lay things out like this:
#include <iostream>
class ConstC
{
int c;
protected:
ConstC(int n): c(n) {}
int get() const { return c; }
};
class A: private ConstC
{
public:
A(int n): ConstC(n) {}
friend std::ostream& operator<< (std::ostream& os, const A& a)
{
return os << a.get();
}
};
int main()
{
A first(10);
A second(20);
std::cout << first << ' ' << second << '\n';
first = second;
std::cout << first << ' ' << second << '\n';
}