Prevent C++ from Assignment and Movement to rvalue

Introduction

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.

complex_1.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <cstdlib>
#include <iostream>

class Complex
{
public:
Complex(double r = 0.0, double i = 0.0) : real{r}, img{i} {}
// This is bad practice.
// Just don't implement implicit conversion.
operator bool() const
{
if (this->real != 0 || this->img != 0)
{
return true;
}
return false;
}
void print() const
{
char symbol = '+';
if (this->img < 0)
{
symbol = '-';
}
std::cout << this->real << " " << symbol << " " << std::abs(this->img)
<< "i" << std::endl;
}
double real, img;
};

Complex operator+(Complex const& lhs, Complex const& rhs)
{
Complex res{lhs.real + rhs.real, lhs.img + rhs.img};
return res;
}

int main()
{
double const x{1.0};
double const 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.

1
2
3
4
5
Complex const operator+(Complex const& lhs, Complex const& rhs)
{
Complex res{lhs.real + rhs.real, lhs.img + rhs.img};
return res;
}

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 ‘int main()’:
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.

complex_2.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <cstdlib>
#include <iostream>

class Complex
{
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.
operator bool() const
{
if (this->real != 0 || this->img != 0)
{
return true;
}
return false;
}
void print() const
{
char symbol = '+';
if (this->img < 0)
{
symbol = '-';
}
std::cout << this->real << " " << symbol << " " << std::abs(this->img)
<< "i" << std::endl;
}
double real, img;
};

int main()
{
double const x{1.0};
double const 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.

1
2
3
4
5
Complex const operator+(Complex const& obj) const
{
Complex res{this->real + obj.real, this->img + obj.img};
return res;
}

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.

complex_3.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <cstdlib>
#include <iostream>

class Complex
{
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.
operator bool() const
{
if (this->real != 0 || this->img != 0)
{
return true;
}
return false;
}
void print() const
{
char symbol = '+';
if (this->img < 0)
{
symbol = '-';
}
std::cout << this->real << " " << symbol << " " << std::abs(this->img)
<< "i" << std::endl;
}
double real, img;
};

int main()
{
double const x{1.0};
double const 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 ‘int main()’:
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;
| ^~~~~~~~

References

Prevent C++ from Assignment and Movement to rvalue

https://leimao.github.io/blog/CPP-rvalue-Assignment/

Author

Lei Mao

Posted on

04-21-2022

Updated on

10-15-2023

Licensed under


Comments