In C++, it is legal to assign or move to rvalue of user defined types. However, if we ever encounter such code, usually the creator did not intended to do it.
In this blog post, I would like to discuss how “dangerous” it is to allow the assignment and movement to rvalue and how to prevent it by throwing compile-time errors.
Return By Const Value
In this example, we implemented a user defined Complex class and this class could be implicitly converted to bool. We also implemented the operator + for Complex by returning non-const value. Finally, we made an typo a + b = d instead of a + b == d in the if condition. This typo is just the assignment to rvalue. Because Complex could be implicitly converted to bool, the if statement is also perfectly valid.
intmain() { doubleconst x{1.0}; doubleconst y{1.0}; double z{0.0}; z = x + y; std::cout << z << std::endl; // Modifying a rvalue? // Illegal for built-in types. // x + y = z; Complex const a{1.0, 2.0}; Complex const b{1.0, -3.0}; Complex c{0.0, 0.0}; Complex d{3.0, 0.0}; c = a + b; c.print(); // Modifying a rvalue? // Legal for user-defined types. // Probably no warnings at all. // This usually mean the program is wrong. a + b = c; // Whoops, a typo in the if condition! // Compiler might throw warnings against this typo. // This should be the correct if condition. // if (a + b == d) if (a + b = d) { std::cout << "a + b == c" << std::endl; } }
It is a legal C++ implementation even if we have a typo in the implementation. The GCC compiler compiled it successfully. But the execution result is not expected due to the typo.
1 2 3 4 5 6 7 8 9
$ g++ complex_1.cpp -o complex_1 -std=c++14 -Wall complex_1.cpp: In function ‘int main()’: complex_1.cpp:63:15: warning: suggest parentheses around assignment used as truth value [-Wparentheses] 63 | if (a + b = d) | ~~~~~~^~~ $ ./complex_1 2 2 - 1i a + b == c
To prevent this scenario, instead of returning by non-const value, we return by const value.
The compiler then throws error because we could not assign to a const temporary rvalue a + b.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
$ g++ complex_1.cpp -o complex_1 -std=c++14 -Wall complex_1.cpp: In function ‘intmain()’: complex_1.cpp:58:13: error: passing ‘const Complex’ as ‘this’ argument discards qualifiers [-fpermissive] 58 | a + b = c; | ^ complex_1.cpp:5:7: note: in call to ‘constexpr Complex& Complex::operator=(const Complex&)’ 5 | class Complex | ^~~~~~~ complex_1.cpp:63:17: error: passing ‘const Complex’ as ‘this’ argument discards qualifiers [-fpermissive] 63 | if (a + b = d) | ^ complex_1.cpp:5:7: note: in call to ‘constexpr Complex& Complex::operator=(const Complex&)’ 5 | class Complex | ^~~~~~~ complex_1.cpp:63:15: warning: suggest parentheses around assignment used as truth value [-Wparentheses] 63 | if (a + b = d) | ~~~~~~^~~
Delete Assignment and Movement to Rvalue
This example is almost the same as the previous example, except that the operator + was implemented as a class member method instead of a static function.
classComplex { public: Complex(double r = 0.0, double i = 0.0) : real{r}, img{i} {} Complex(const Complex& rhs) : real{rhs.real}, img{rhs.img} {}; Complex operator+(Complex const& rhs) const { Complex res{this->real + rhs.real, this->img + rhs.img}; return res; } // This is bad practice. // Just don't implement implicit conversion. operatorbool()const { if (this->real != 0 || this->img != 0) { returntrue; } returnfalse; } voidprint()const { char symbol = '+'; if (this->img < 0) { symbol = '-'; } std::cout << this->real << " " << symbol << " " << std::abs(this->img) << "i" << std::endl; } double real, img; };
intmain() { doubleconst x{1.0}; doubleconst y{1.0}; double z{0.0}; z = x + y; std::cout << z << std::endl; // Modifying a rvalue? // Illegal for built-in types. // x + y = z; Complex const a{1.0, 2.0}; Complex const b{1.0, -3.0}; Complex c{0.0, 0.0}; Complex d{3.0, 0.0}; c = a + b; c.print(); // Modifying a rvalue? // Legal for user-defined types. // Probably no warnings at all. // This usually mean the program is wrong. a + b = c; // Whoops, a typo in the if condition! // Compiler might throw warnings against this typo. // This should be the correct if condition. // if (a + b == c) if (a + b = d) { std::cout << "a + b == c" << std::endl; } }
It is also a legal C++ implementation even and the execution result is not expected due to the typo a + b = d in the if condition.
1 2 3 4 5 6 7 8 9
$ g++ complex_2.cpp -o complex_2 -std=c++14 -Wall complex_2.cpp: In function ‘int main()’: complex_2.cpp:72:15: warning: suggest parentheses around assignment used as truth value [-Wparentheses] 72 | if (a + b = d) | ~~~~~~^~~ $ ./complex_2 2 2 - 1i a + b == c
We could do the similar thing to prevent assignment to rvalue as we did in the previous example.
However, a more modern approach is to delete the assignment to rvalue. In addition, we could also delete the move to rvalue, since moving to a temporary value does not make sense, either.
classComplex { public: Complex(double r = 0.0, double i = 0.0) : real{r}, img{i} {} Complex(const Complex& rhs) : real{rhs.real}, img{rhs.img} {}; Complex operator+(Complex const& rhs) const { Complex res{this->real + rhs.real, this->img + rhs.img}; return res; } // Delete copy assignment to rvalue. Complex& operator=(Complex const& rhs) && = delete; // Delete move assignment to rvalue. Complex& operator=(Complex&& rhs) && = delete; // Explicitly enable copy assignment to lvalue. // This is required. Complex& operator=(Complex const& rhs) & = default; // Explicitly enable move assignment to lvalue. // This is required. Complex& operator=(Complex&& rhs) & = default; // This is bad practice. // Just don't implement implicit conversion. operatorbool()const { if (this->real != 0 || this->img != 0) { returntrue; } returnfalse; } voidprint()const { char symbol = '+'; if (this->img < 0) { symbol = '-'; } std::cout << this->real << " " << symbol << " " << std::abs(this->img) << "i" << std::endl; } double real, img; };
intmain() { doubleconst x{1.0}; doubleconst y{1.0}; double z{0.0}; z = x + y; std::cout << z << std::endl; // Modifying a rvalue? // Illegal for built-in types. // x + y = z; Complex const a{1.0, 2.0}; Complex const b{1.0, -3.0}; Complex c{0.0, 0.0}; Complex d{3.0, 0.0}; c = a + b; c.print(); // Modifying a rvalue? // Legal for user-defined types. // Probably no warnings at all. // This usually mean the program is wrong. a + b = c; // Whoops, a typo in the if condition! // Compiler might throw warnings against this typo. // This should be the correct if condition. // if (a + b == c) if (a + b = d) { std::cout << "a + b == c" << std::endl; } }
The compiler then throws error because the assignment to rvalue member method has been deleted.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$ $ g++ complex_3.cpp -o complex_3 -std=c++14 -Wall complex_3.cpp: In function ‘intmain()’: complex_3.cpp:67:13: error: use of deleted function ‘Complex& Complex::operator=(const Complex&) &&’ 67 | a + b = c; | ^ complex_3.cpp:15:14: note: declared here 15 | Complex& operator=(Complex const& rhs) && = delete; | ^~~~~~~~ complex_3.cpp:72:17: error: use of deleted function ‘Complex& Complex::operator=(const Complex&) &&’ 72 | if (a + b = d) | ^ complex_3.cpp:15:14: note: declared here 15 | Complex& operator=(Complex const& rhs) && = delete; | ^~~~~~~~