C++ Reference Collapsing

Introduction

Because C++ template and type deduction, it is possible to have reference to reference in templates or typedefs. However, the resulting type of reference to reference could be confusing.

In this blog post, I would like to discuss the reference collapsing rules in C++.

Reference Collapsing

Reference Collapsing Rules

It is permitted to form references to references through type manipulations in templates or typedefs. rvalue reference to rvalue reference collapses to rvalue reference, all other combinations form lvalue reference.

  • T&& &&T&&
  • T& &&T&
  • T& &T&
  • T&& &T&

Universal Reference

Scott Meyers proposed the concept of “universal reference” to facilitate the user to understand T&& in C++ and distinguish T&& from conventional rvalue reference.

For example, the following is a declaration with universal reference.

1
2
template<typename T>
void f(T&& param);

Universal reference states that if param is a rvalue reference, T&& will just serve as T&&, otherwise, T&& will just serve as T&. However, technically universal reference is just rvalue reference to rvalue/lvalue reference and applying reference collapsing.

The only caveat for the above example is that

1
2
int param{0};
f(param); // T& && -> T&, instead of T && -> T&&

auto

1
2
3
std::vector<int> vec_0{0, 1, 2};
auto&& vec_1{vec_0}; // auto&& -> std::vector<int>& && -> std::vector<int>&
auto&& vec_2{std::move(vec_0)}; // auto&& -> std::vector<int>&& && -> std::vector<int>&&

Type Alias

1
2
3
4
5
6
7
typedef int&  lref;
typedef int&& rref;
int n;
lref& r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref& r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&

In the scenario where type aliasing using typedef is replaced with type aliasing using using, the reference collapsing rule still applies.

1
2
3
4
5
6
7
using lref = int&;
using rref = int&&;
int n;
lref& r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref& r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&

decltype

decltype(expr) yields T, T&, and T&&, depending on expr. The rules is specified at the decltype CPP reference webpage.

If the argument is an unparenthesized id-expression or an unparenthesized class member access expression, then decltype yields the type of the entity named by this expression.

If the argument is any other expression of type T, and

  • if the value category of expression is xvalue, then decltype yields T&&;
  • if the value category of expression is lvalue, then decltype yields T&;
  • if the value category of expression is prvalue, then decltype yields T.

While xvalue, prvalue, and rvalue have been confusing to the user, some examples might just be straightforward to understand.

1
2
3
4
std::vector<int> vec_0{0, 1, 2};
decltype(vec_0) vec_1{vec_0}; // id-expression -> std::vector<int>
decltype((vec_0)) vec_2{vec_0}; // lvalue -> std::vector<int>&
decltype(std::move(vec_0)) vec_3{std::move(vec_0)}; // xvalue -> std::vector<int>&&

Notice that (vec_0) is a lvalue expression (parenthesized expression if the unparenthesized expression is an lvalue) whereas vec_0 is a rvalue expression.

The reference collapsing rules also applies for types deduced by decltype.

1
2
3
4
std::vector<int> vec_0{0, 1, 2};
decltype(vec_0)&& vec_1{std::move(vec_0)}; // std::vector<int>&&
decltype((vec_0))&& vec_2{vec_0}; // std::vector<int>& && -> std::vector<int>&
decltype(std::move(vec_0))&& vec_3{std::move(vec_0)}; // std::vector<int>&& && -> std::vector<int>&&

References

Author

Lei Mao

Posted on

10-06-2022

Updated on

10-06-2022

Licensed under


Comments