In previous articles we’ve discussed how to make objects “move-aware” so that moves are more efficient than copies, and also how to forward objects efficiently between different scopes. In this article we’ll be looking at how different member function overloads can be called based on the (reference and const-ness) properties of the object itself.
Even novice C++ programmers may be familiar with the term “const-correctness” by its application to writing member functions with or without a const qualifier (in other words, pairs of member functions: which one being called depending on the const-ness of the object). The amount of code duplication can be minimized by using the pattern popularized by Scott Meyers, where the non-const version calls the const version with a static_cast of *this (to const T&), and further uses a const_cast on any return value.
With the advent of r-value semantics, the non-const and const variants can be usefully replaced with reference-ness qualifiers for & (non-const reference), const & (const reference) and && (r-value reference). (The existence of these three qualifiers are incompatible with plain non-const and const variants for the same function.)
Below is an outline of a class which overloads a function show() to make three different variants based on the reference type of the *this object. It’s been defined as a struct in order to give public access by default, and could be used as a base class if desired. Note all five special member functions plus the virtual destructor have been defaulted (in order to generate them all):
struct Qualifiers {
Qualifiers() = default;
Qualifiers(const Qualifiers&) = default;
Qualifiers(Qualifiers&&) = default;
Qualifiers& operator=(const Qualifiers&) = default;
Qualifiers& operator=(Qualifiers&&) = default;
virtual ~Qualifiers() = default;
void show() & {
std::cout << "Qualifiers::show() &\n";
}
void show() const & {
std::cout << "Qualifiers::show() const &\n";
}
void show() && {
std::cout << "Qualifiers::show() &&\n";
}
};
The following program uses this struct and shows how all three overloads can be invoked:
#include "Qualifiers.hpp"
int main() {
Qualifiers q1, &q2 = q1;
const Qualifiers q3, &q4 = q3;
q1.show();
q2.show();
q3.show();
q4.show();
std::move(q1).show();
Qualifiers().show();
}
The output from running this program is:
Qualifiers::show() &
Qualifiers::show() &
Qualifiers::show() const &
Qualifiers::show() const &
Qualifiers::show() &&
Qualifiers::show() &&
This demonstrates that reference-ness can be “simulated” for the purposes of these overloads for the “real” objects (q1 and q3), and that references to these objects (q2 and q4) call the same overloads as for the objects themselves. There should be no surprises at the link between non-const objects and overload &, and between const objects and overload const &. The && overload is only called when invoking show() on a temporary; in the case of a real class having a get() method for example, this could be overloaded to std::move() member data into the return value as the object itself will not outlast the call.
A feature of C++ references is that there is no such thing as a “reference to a reference”. Thus, in the code below, variable c is directly a reference of variable a:
int a = 99;
int& b = a;
int& c = b;
Where a reference is created from another reference, a process known as reference collapsing takes place. If one or both are l-value references, an l-value reference (possibly const) is generated, while if both are r-value references, an r-value reference is created. Reference collapsing is not usually a feature in function calls because the parameter and argument reference-ness must match exactly (with the relaxation of allowing const references to non-const variables to be created). When using perfect forwarding with std::forward from a universal reference (T&&) parameter to a function taking any of T&, const T& or T&&, these reference collapsing rules will apply.
That’s about it for this article and this mini-series. We’ve seen how to overload member functions based on “reference-ness” rather than “const-(correct)-ness”, and have mentioned reference collapsing. You should now be equipped to use both l-value references and r-value references (in some contexts called universal references) in your code, and understand how to differentiate between, and combine them, usefully.