C++ 一到店,所有人都便都看着他笑……「对呀对呀!……引用有四种写法,你知道么?」

没有引用

编程初学者写一个交换函数,很可能是这样的

1
2
3
4
5
6
void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}

显然这样是不起作用的。有些人会从汇编的角度分析,有些人会从调用方式的角度分析。

最后修正后的 C 语言版本使用了指针。(使用宏的都拖出去续了)

1
2
3
4
5
6
void swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}

因为不恰当的使用指针很容易造成空指针的问题。所以 C++ 引入了引用。

左值引用

C++ 使用引用版本的交换函数

1
2
3
4
5
6
void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}

引用本质上是自动解引用的指针。所以这段代码跟指针的版本原理差不多。

这时候的 C++ 还是守序善良。短短几年过去却变成了混乱邪恶的 C++ 11 。

右值引用

C++ 11 引入了右值引用,用 && 表示,对应的之前的引用称为左值引用,用 & 表示。使用右值引用的交换函数版本

1
2
3
4
5
6
void swap(int&& x, int&& y)
{
int temp = x;
x = y;
y = temp;
}

然而内置类型并没有移动构造函数,所以移动退化为复制。C++ 11 之前都是拷贝交换,C++ 11 引入右值引用后才实现了真正上的移动交换。如果使用自定义类区别就会明显一些。

通用引用

通用的移动交换函数应该是这样的

1
2
3
4
5
6
template<typename T> 
void swap(T& x, T& y) {
T temp = std::move(x);
x = std::move(y);
y = std::move(temp);
}

T 可以是 intint &int && 等等,代入后可能特化为以下版本

1
2
3
void swap(int & x, int & y);
void swap(int& &x, int& & y);
void swap(int&& &x, int&& &y);

这样就出现了两个问题。

本来只有左值引用的时候模板函数只需要一两个特化版本。现在引入右值引用后还要写一个版本。

第二个问题是 int&& & 是什么类型?

为了解决第一个问题,C++ 11 引入了通用引用,用 && 表示。使用通用引用的版本

1
2
3
4
5
6
template<typename T> 
void swap(T&& x, T&& y) {
T temp = std::move(x);
x = std::move(y);
y = std::move(temp);
}

那么问题又来了。通用引用(&&)和右值引用(&&)是一样的吗?

答案是不一样的。那要怎么区分呢?

第二个问题的回答刚好解决了这个新问题。

实际上 C++ 是不允许创建引用的引用的。所以右值引用才可以用 && 表示,不然到底是左值引用的左值引用还是单纯的右值引用?

但在 C++ 中还是有办法绕过限制,比如上面使用模板的方法来实现 int& & 的效果。

创建引用的引用是没有意义的,直接使用引用即可。所以有以下规约

1
2
3
4
& & -> &
&& & -> &&
& && -> &
&& && -> &&

其中 & && -> &&& && -> && 都不会改变原有引用的左右性,所以参数声明为 && 就表示,如果是左值引用,那么传进来还是左值引用。如果是右值引用,那么传进来还是右值引用。

因此 Scott Meyers 把 && 命名为通用引用。

相应地模板只需要写一个 T&& 的特化版就可以了,这样的特性被称为完璧转发。

从规约上看,具体的类型如 int&& 就是右值引用,通用的类型 T&& 则是通用引用,左右性需要看传入参数的左右性。

文章引用

通用引用的定义

右值引用和通用引用的区别