
Return by value and const variables in C ++ 11
In many programming languages, it is possible to declare objects and variables constant. And, accordingly, there are recommendations to do so if you are not going to change their values. With the advent of the new standard, a recommendation appeared in C ++ to return objects from functions by value, because even without RVO you can improve program performance by using the semantics of movement. What will happen if we use these two recommendations together: return a constant object by value? Let's try to figure it out further.
Watching videos from the recent C ++ Now conference, I came across one interesting point (pitfall). At the end of one of the speeches, the speaker gives the following code:
and asks what happens to the object r when it returns from the function?
Suppose that the structure s has both a copy constructor and a move constructor, and the function f contains code that impedes the use of RVO. What happens to the object r when it returns from the function? If the object is created without the const specifier , then when it returns from the function, it will be moved, and if created with const , it will be copied . Similar behavior was noticed in GCC and in Clang (in Visual Studio I did not check). What is it: bug or expected behavior? If an object is immediately destroyed, why not move it?
Since C ++ 03, many of us are accustomed to the fact that temporary objects and constants are almost the same thing. And this is logical, because the behavior of both of them was similar: they allowed you to call only functions that take arguments by value or constant reference (or simply assign their values to these types):
In C ++ 11, the situation has changed: now we can change temporary objects using rvalue references. But not constants. According to the standard, any change to objects declared with the const specifier (whether it be using const_cast or perversions with pointers) leads to undefined behavior. It turns out that if the compiler generated the code using the move constructor, it would put the program in a state of undefined behavior, which is unacceptable. In other words, const-correctness has a higher priority for the compiler than similar optimization. Still, C ++ is a strongly typed language and any implicit casts of references and pointers from const to normal are forbidden, for example: const A a -> A && .
Consider the following code:
The C ++ standard does not allow implicitly removing qualifiers from objects. If a certain type of A can be reduced to either A & or to const A & , then const A can only be reduced to const A & . Now replace the lvalue of the link with the rvalue of the link by adding std :: move calls when passing parameters to functions ... Exactly! We cannot cast const A a to A && , but we can cast const A && . If you write such a constructor, the compiler uses it to return constant variables from functions, but there is no more sense in this than from a regular copy constructor. Therefore, this type of link is usually not used.
As it turns out, some recommended practices are not combined. In C ++, you constantly have to watch here and there, so as not to stumble on another pitfall. Now you need to follow the constants so that they do not complicate your life. After all, no one is safe from such a code (and even if you rely on RVO and think that this case is not important to you, you can spend a lot of time looking for the reason why this code does not compile if there is no copy constructor in the s structure):
And even in this code there is a copy of the object (another pitfall):
Prefer this option:
Or, only if you value each move constructor:
Do not confuse the previous example with the following code that returns an rvalue link:
Never do this, here returns a link to an already destroyed object.
And may the Force be with you!
Watching videos from the recent C ++ Now conference, I came across one interesting point (pitfall). At the end of one of the speeches, the speaker gives the following code:
struct s
{
...
};
s f()
{
const s r;
...;
return r;
}
and asks what happens to the object r when it returns from the function?
Suppose that the structure s has both a copy constructor and a move constructor, and the function f contains code that impedes the use of RVO. What happens to the object r when it returns from the function? If the object is created without the const specifier , then when it returns from the function, it will be moved, and if created with const , it will be copied . Similar behavior was noticed in GCC and in Clang (in Visual Studio I did not check). What is it: bug or expected behavior? If an object is immediately destroyed, why not move it?
Since C ++ 03, many of us are accustomed to the fact that temporary objects and constants are almost the same thing. And this is logical, because the behavior of both of them was similar: they allowed you to call only functions that take arguments by value or constant reference (or simply assign their values to these types):
void f1(int a) { }
void f2(int& a) { }
void f3(const int& a) { }
int g() { return 0; }
int main()
{
f1(g()); // OK
f2(g()); // compile error
f3(g()); // OK
const int a = 0;
f1(a); // OK
f2(a); // compile error
f3(a); // OK
}
In C ++ 11, the situation has changed: now we can change temporary objects using rvalue references. But not constants. According to the standard, any change to objects declared with the const specifier (whether it be using const_cast or perversions with pointers) leads to undefined behavior. It turns out that if the compiler generated the code using the move constructor, it would put the program in a state of undefined behavior, which is unacceptable. In other words, const-correctness has a higher priority for the compiler than similar optimization. Still, C ++ is a strongly typed language and any implicit casts of references and pointers from const to normal are forbidden, for example: const A a -> A && .
Consider the following code:
void f1(int& a) { }
void f2(const int& a) { }
int main()
{
int a1 = 0;
f1(a1); // OK
int a2 = 0;
f2(a2); // OK
const int ca1 = 0;
f1(ca1); // compile error
const int ca2 = 0;
f2(ca2); // OK
}
The C ++ standard does not allow implicitly removing qualifiers from objects. If a certain type of A can be reduced to either A & or to const A & , then const A can only be reduced to const A & . Now replace the lvalue of the link with the rvalue of the link by adding std :: move calls when passing parameters to functions ... Exactly! We cannot cast const A a to A && , but we can cast const A && . If you write such a constructor, the compiler uses it to return constant variables from functions, but there is no more sense in this than from a regular copy constructor. Therefore, this type of link is usually not used.
Conclusion
As it turns out, some recommended practices are not combined. In C ++, you constantly have to watch here and there, so as not to stumble on another pitfall. Now you need to follow the constants so that they do not complicate your life. After all, no one is safe from such a code (and even if you rely on RVO and think that this case is not important to you, you can spend a lot of time looking for the reason why this code does not compile if there is no copy constructor in the s structure):
struct s
{
...
};
s foo();
s bar()
{
const auto r = foo();
if (r.check_something())
throw std::exception();
return r;
}
And even in this code there is a copy of the object (another pitfall):
s bar()
{
auto&& r = foo();
...;
return r;
}
Prefer this option:
s bar()
{
auto r = foo();
...;
return r;
}
Or, only if you value each move constructor:
s bar()
{
auto&& r = foo();
...;
return std::move(r);
}
Note
Do not confuse the previous example with the following code that returns an rvalue link:
s&& bar()
{
auto&& r = foo();
...;
return std::move(r);
}
Never do this, here returns a link to an already destroyed object.
And may the Force be with you!