Understanding Reference Collapsing and Perfect Forwarding in C++
Written on
Chapter 1: Introduction to Reference Collapsing
In C++, reference collapsing rules, such as the transformation A& & -> A&, exist primarily to facilitate perfect forwarding. "Perfect forwarding" refers to the ability to forward parameters as if the function were called directly by the user, excluding elision which is disrupted by forwarding. Users can pass three types of values: lvalues, xvalues, and prvalues. Correspondingly, the receiving function can accept these values either by value, by lvalue reference, or by rvalue reference.
This article will explore the concepts of reference collapsing and perfect forwarding, aiming to enhance understanding of these crucial programming mechanisms.
Section 1.1: What is Reference Collapsing?
Reference collapsing pertains to how multiple references in template parameters and auto type deduction interact with each other. Let’s illustrate this with a straightforward example:
template <typename T>
void f(T &t) {}
void test() {
int a = 3;
f(a);
f(a);
f(a);
}
When T is specified as int, the function f transforms to:
void f(int &t);
However, what happens when T is specified as int & or int &&? One might assume two potential outcomes:
void f(int & &t);
void f(int && &t);
In reality, compiling this scenario does not yield an error. The reference collapsing rules dictate that:
- An lvalue reference takes precedence over an rvalue reference.
Simply put, unless two rvalue references are present, resulting in an rvalue reference, all other cases yield an lvalue reference. Thus, the rules are as follows:
& + & -> &
& + && -> &
&& + & -> &
&& + && -> &&
The same rules apply to auto &&. When auto && encounters an lvalue, it deduces as an lvalue reference, and it only deduces as an rvalue reference when it encounters an rvalue:
auto &&r1 = 5; // Deduces int &&
int a;
auto &&r2 = a; // Deduces int &
int &&b = 1;
auto &&r3 = b; // Deduces int &
Due to the precedence of & over &&, auto & will always deduce as an lvalue reference. Binding auto & to a constant or temporary object will yield an error:
auto &r1 = 5; // Error, cannot bind lvalue reference to constant
auto &r2 = GetAnObj(); // Error, cannot bind lvalue reference to temporary object
However, an lvalue reference can bind to an rvalue reference:
int &&b = 1;
auto &r3 = b; // OK, lvalue reference can bind to rvalue reference
auto &r4 = r3; // OK, lvalue reference can bind to lvalue reference
Section 1.2: Understanding Rvalue Property Loss
Once an rvalue reference is bound, it loses its rvalue property. Consider this example:
void f1(int &&t1) {}
void f2(int &&t2) {
f1(t2); // Note this}
void Demo() {
f2(5);}
In the Demo function, f2 is called with an rvalue reference, which can bind to the literal 5 without any issues. Yet, within the f2 function, t2 is treated as an lvalue reference because once bound, an rvalue reference becomes an lvalue. Thus, passing t2 to f1 will lead to an error. This exemplifies the concept of "losing rvalue property in rvalue reference passing."
To maintain the rvalue property, you can utilize std::move:
void f1(int &&t1) {}
void f2(int &&t2) {
f1(std::move(t2)); // Preserve rvalue property}
void Demo() {
f2(5);}
Chapter 2: The Concept of Perfect Forwarding
In template functions, we often want to maintain the characteristics of parameters when passing them. Perfect forwarding helps achieve this, as illustrated below:
template <typename T>
T &forward(T &t) {
return t; // Directly pass out if lvalue}
template <typename T>
T &&forward(T &&t) {
return std::move(t); // Preserve rvalue property if rvalue}
template <typename T>
void f1(T &&t1) {}
template <typename T>
void f2(T &&t2) {
f1(forward(t2));}
void test() {
f2(5); // Pass rvalue
int a;
f2(a); // Pass lvalue
}
The std::forward function is termed "perfect forwarding," and it aims to maintain the characteristics of references while forwarding them. This technique is particularly advantageous when creating generic functions or classes.
The first video, C++ 11: Rvalue Reference - Perfect Forwarding, provides a comprehensive overview of how rvalue references work and their importance in C++ programming.
The second video, Why Use Forwarding References and Their Differences from Rvalue References in C++, delves into the distinctions between forwarding references and rvalue references, enhancing your understanding of these concepts.
Conclusion
This article has explored the intricate concepts of reference collapsing and perfect forwarding in C++. Despite their complexity, these techniques are vital for improving the flexibility and efficiency of your code. It is our hope that this discussion aids readers in grasping and applying these principles in their C++ programming endeavors.