引用与指针

指针一方面是为了兼容C的程序,另一方面指针的功能非常强大,这里不做过多介绍了,指针就是变量的地址。

左右值

int a = 1; // 这里 a 写在左边,就是左值,1写在右边,就是右值
int b = a; // 左值:b,右值:a

基于上述例子,可以看出,大部分变量其实是左值,变量也可以当右值,但是显然数字这种量不能当左值,比如不能写1=a这样的语法。

左右值很大一个区别就是,左值可以取地址&a,右值是不能取地址的&1

右值还有一个名称,叫做将亡值,即生命周期马上就没了的。

但是经常会有使用右值的情况,因此右值已经会被移动了,可以把右值看成无人认领的物品,如果有人认领,右值就会被移动,没人认领就消失了。

引用

c++11之前只有一种引用,也就是现在的左值引用,不妨把左值引用叫做绑定吧。

int a = 1;
int& b = a;  // 把左值引用 b 绑定到 a 上,对b修改就会修改a
b = 2; // a也变成了2,事实上,引用底层实现就是用了地址。

左右值引用

  • 左值引用只能绑定左值,不能绑定右值。
  • const左值引用可以绑定左值或右值。
  • 右值引用只能绑定右值,不可以绑定左值。

move

c++给出了一个函数,move函数,可以把左值更换成右值的属性

#include <move.h>
int b = 1;
// int&& c = b; // 报错
int&& c = std::move(b); // 不报错,可运行
int&& d = c;  // 也报错,右值引用变量其实是左值

拷贝/移动 构造/赋值

常常定义类的时候,会有拷贝构造和移动构造,拷贝赋值运算符重载和移动赋值运算符重载等。

  • 拷贝构造/赋值参数是:const T& t,能够接收左值和右值
  • 移动构造/赋值参数是:T&& t() noexcept,只能接收右值,并推荐加上noexcept来提高运行速度
  • 如果传入的是右值,编译器会更加倾向于调用移动构造/赋值,除非没有定义移动赋值和构造。

如果一个对象(包括函数的形参声明)声明的时候用另一个对象赋值,就会调用构造函数而不是赋值函数。如果一个变量已经声明且定义了,在后续赋值则会调用赋值运算符。

在现代编译器上,一般都会有优化,所以不用特别担心,总之,相信编译器的优化吧。

forward

万能引用-forward的引子

void function(MyClass&& a);  // 这是右值引用

template<class T>
void function(T&& t); // 这里其实叫做万能引用,因为T的类型需要等调用才确定

template <class T>
class A{
    void function_2(T&& t);  // 这里其实是右值引用,可不是万能引用,因为实例化对象就已经确定T的类型

    template<class U>  // 模板嵌套相当于 template <class T> template<class U> void fun();
    void function_3(U&& u);  // 万能引用,因为U的类型要等到调用才知道
};

万能引用中,如果传入左值,那么是左值引用,传入右值就是右值引用。

保持右值属性

前面提到,右值引用的变量其实是左值,那如何保证其右值属性好为我们所用呢?答:std::forward<T>()转发函数

#include <move.h>
int a = 1;
int& b = a;
int&& c = move(b);
int&& d = forward<int>(c); // 其实 forward 返回的就是右值罢了

万能引用的应用

void Fun(int& a) {
    cout << "左值引用,a = " << a << endl;
}
void Fun(int&& a) {
    cout << "右值引用,a = " << a << endl;
}
void Fun(const int& a) {
    cout << "const左值引用,a = " << a << endl;
}
void Fun(const int&& a) {
    cout << "const右值引用,a = " << a << endl;
}

template<class T>
void F(T&& t) {  // 这是一个万能引用
    Fun(std::forward<T>(t));  // 保持右值属性
    Fun(t);  // 如果不使用转发的话,t就一定是个左值,因为右值引用,左值引用都是左值,这样我们一旦进入深层函数就没法保持右值属性了
}

int a = 10;
const int b = 11;
const int c = 12;

// 调用
F(a);
F(b);
F(std::move(c));
F(13);

结果如下:

左值引用,a = 10       // a
const左值引用,a = 11  // b
const右值引用,a = 12  // c
右值引用,a = 13       // 13

分析:

  1. a显然是左值,调用的也是左值引用
  2. b是常左值,调用的也是常左值引用
  3. move(c)是右值引用,然后前面还有const修饰,所以是常右值引用
  4. 13是典型的右值,所有是右值引用

STL库应用参考

上面是特性,下面给出STL库中怎么使用forward的呢?

template<class T>
class vector{
    void push_back(const T& t){  // 左值引用
        insert(end(), t);
    }

    void push_back(T&& t){  // 右值引用
        insert(end(), std::forward<T>(t)); // 完美转发保持右值属性
        // 解释:因为 这个函数里面的t是一个右值引用的左值量,如果直接传入insert的话,就变成左值了
        //      因此,这里使用forward保证它的右值属性
    }
}

// 这里的insert是形如下面的定义
template<class T>
void insert(Iterator it, T&& t); // 用的万能引用

也就是说,如果我们希望传入右值引用还保持右值属性的话,就需要使用forward转发

拓展

上面的结论是forward可以保持右值引用的属性,但是看了下面定义,发现还可以干点其他的

// 下面是forward的定义
template<typename _Tp>

constexpr _Tp&&  // 返回类型是<T>里面T的右值引用

forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

template<typename _Tp>
struct remove_reference
{ using type = __remove_reference(_Tp); };  // __remove_reference找不到具体定义,应该是编译器的,但是我们根据名字可以直到是去除引用属性的

有了上面的可以干嘛呢?其实引用符号是可以消掉的,比如&&&等价于&(但是明着这么写是会报错的,编译器处理的逻辑确实这样),也就是,奇数个引用都是左值引用,偶数个引用(不包括0)都是右值引用。

int a = 10;

// 下面两个是赋值,都不会报错
int b = forward<int&>(a);
int b = forward<int>(a);

int& b = forward<int>(a); // 报错的
int& b = std::forward<int&>(a); // 这个却不报错
int&& b = forward<int>(a); // 不报错
int&& b = forward<int&>(a); // 报错
int&& b = forward<int&&>(a); // 不报错

// 在代码中是不允许连续写多个 & 的,但是编译器转换其实又是采用这种方式

// 规律
// b前面的 & 数量的奇偶性需要和 <&> 里面的奇偶性保持一致,<&>里面为0也算是偶数

继续拓展

回到上面的万能函数

template<class T>
void F(T&& t);

F<int&>(a);  // 不报错,编译器处理 T=int&, T&&=int&&&=int&
F<int&&>(1); // 也不报错,编译器处理 T=int&&, T&&=int&&&&=int&&

总结

量分为左值量和右值量,引用分为左值引用和右值引用。

  1. 左值量绑定到左值引用,可以
  2. 右值量绑定到左值引用,不可以(需要常左值引用修饰才可以,其实是为了兼容c++11以前的代码采取的措施)
  3. 左值量绑定到右值引用,不可以(要使用move转换,其作用是将左值变成右值)
  4. 右值量绑定到右值引用,可以
  5. 右值引用也是一个左值量,需要使用forward<T>(T t)来将它转换成右值。

对于一些函数的写法:

template<class T> 
class A{
  void function(T&& t); // 右值引用
  template<class U> void function_2(U&& u); // 嵌套模板,里层仍然是未知类型,还是万能引用
};
template<class T> void function(T&& t); // 万能引用

使用forward的情景是:希望保持右值属性的时候使用它即可