Have you ever used member functions with reference qualifiers? This a feature
introduced in C++11, being an essential part of move semantics.
Short recap
C++11 allows for specifying either lvalue or rvalue reference qualifier on
a non-static member function. The reference qualifier is part of an overload
resolution so, it’s possible to have the following:
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
|
class Box {
public:
void abc() & {
std::cout << "lvalue variant" << std::endl;
}
void abc() && {
std::cout << "rvalue variant" << std::endl;
}
};
Box foo() {
return Box{};
}
int main() {
Box b;
// lvalue
b.abc();
// rvalues
std::move(b).abc();
Box{}.abc();
foo().abc();
return 0;
}
|
Move semantics is not only for function arguments
Now, when would that be of any use? Imagine a data container:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Box {
public:
const std::vector<int>& getData() const {
return lots_of_data;
}
private:
std::vector<int> lots_of_data;
};
int main() {
Box b;
auto data = b.getData();
return 0;
}
|
You often provide read only access as an optimal solution, preventing copying
the vector. There’s nothing wrong with that but now, it’s possible to
complement that with move semantics:
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
|
class Box {
public:
// overload #0
const std::vector<int>& getData() const & {
return lots_of_data;
}
// overload #1
std::vector<int> getData() & {
return lots_of_data;
}
// overload #2
std::vector<int> getData() && {
return lots_of_data;
}
private:
std::vector<int> lots_of_data;
};
void use(const Box& b) {
// will use overload #0
b.getData();
}
int main() {
Box b;
use(b);
// will use overload #1
auto copy = b.getData();
// will use overload #2, b is now an empty husk
auto data = std::move(b).getData();
return 0;
}
|
I’ve seen this trick for the first time used in Meta’s Folly library’s Future implementation.
In short, you use the rvalue overload to convey that the data is gonna be
moved out therefore, you relinquish the ownership, transferring it via the
return value to the caller.
C++23 and explicit object parameter
C++23 introduces an interesting feature which is somewhat related to member
function’s reference qualifiers - explicit object parameter. Here’s a short overview:
1
2
3
4
5
6
7
8
9
10
11
|
class Box {
public:
// This is the same as
// const std::vector<int>& getData() const &;
const std::vector<int>& getData(this const Box& b) {
return b.lots_of_data;
}
private:
std::vector<int> lots_of_data;
};
|
It allows for naming this
explicitly within the member functions. this is
no longer an implicit variable in such function. Object data access has to
happen through the object variable (which now is a reference, not a pointer).
By default named this
is now a reference but it’s possible to pass the object
by value (very much similar to value receivers in golang) to member functions:
1
2
3
4
5
6
|
std::vector<int> getData(this Box b) {
// b is a copy here
// local modifications don't affect the original object
b.lots_of_data.push_back(123);
return b.lots_of_data;
}
|
But what’s the benefit of having that? Well, you can now have a common function handling all three overloads:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
template <typename Self>
auto getData(this Self&& self) {
if constexpr (std::is_rvalue_reference_v<Self&&>) {
std::cout << "move" << std::endl;
return std::move(self.lots_of_data);
} else if (std::is_same_v<Self&&, const Box&>) {
std::cout << "constref" << std::endl;
return self.lots_of_data;
} else {
std::cout << "copy" << std::endl;
return self.lots_of_data;
}
}
|
Or even more conveniently, just drop the template and use auto
:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
auto getData(this auto&& self) {
using Self = decltype(self);
if constexpr (std::is_rvalue_reference_v<Self&&>) {
std::cout << "move" << std::endl;
return std::move(self.lots_of_data);
} else if (std::is_same_v<Self&&, const Box&>) {
std::cout << "constref" << std::endl;
return self.lots_of_data;
} else {
std::cout << "copy" << std::endl;
return self.lots_of_data;
}
}
|