0%

从一次重载失败探秘左值与右值

背景

我写了一个矩阵加减的程序,其中main.cpp如下:

// main.cpp
#include <iostream>
#include "matrix.h"

int main() {
double v[12] {
1, 2, 3, 4,
5, 6, 7, 8,
9,10,11,12};
matrix m1(3, 4, v);
matrix m2 = 2 * m1;

std::cout << "m1 = \n" << m1 << std::endl;
std::cout << "m2 = \n" << m2 << std::endl;
std::cout << "m1 + m2 = \n" << m1 + m2 << std::endl;

return 0;
}

他创建了两个矩阵对象m1m2,分别打印m1m2以及m1 + m1。如果注释掉std::cout << "m1 + m2 = \n" << m1 + m2 << std::endl;,可以正常执行,输出结果如下:

m1 =
1 2 3 4
5 6 7 8
9 10 11 12
g
m2 =
2 4 6 8
10 12 14 16
18 20 22 24

但是一旦加上这句,就会报如下错误:

➜  matrix-rref g++ main.cpp matrix.cpp -o main
main.cpp: In function ‘int main()’:
main.cpp:14:30: error: no match for ‘operator<<’ (operand types are ‘std::basic_ostream<char>’ and ‘matrix’)
14 | std::cout << "m1 + m2 = \n" << m1 + m2 << std::endl;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^~ ~~~~~~~
| | |
| | matrix
| std::basic_ostream<char>
In file included from main.cpp:2:
matrix.h:25:24: note: candidate: ‘std::ostream& operator<<(std::ostream&, matrix&)’ <near match>
25 | friend std::ostream& operator << (std::ostream &out, matrix& m);
| ^~~~~~~~
matrix.h:25:24: note: conversion of argument 2 would be ill-formed:
main.cpp:14:36: error: cannot bind non-const lvalue reference of type ‘matrix&’ to an rvalue of type ‘matrix’
14 | std::cout << "m1 + m2 = \n" << m1 + m2 << std::endl;
| ~~~^~~~

意思是找不到参数列表的为‘std::basic_ostream<char>matrix的输出流重载。起初,我以为是运算符优先级的问题,但发现cout << 1 + 3 << endl这种是能正常输出结果的。这次错误的原因编译器已经告诉我们了:cannot bind non-const lvalue reference of type ‘matrix&’ to an rvalue of type ‘matrix’,即:无法将的左值matrix引用引用绑定到matrix右值。

我检查了我重载的输出流函数原型:

friend std::ostream& operator << (std::ostream &out, matrix &m);

发现matrix &m前没有加上const修饰,记得课上看到的输出输出流重载的参数写法都是const T& v,或许和这有关?把函数修改为:
friend std::ostream& operator << (std::ostream &out, const matrix &m);

再次编译,果然编译通过了,成功运行。以前的写法都是照着博客上的写法依葫芦画瓢,没有深究这里const修饰符的意义,但实际上这里涉及到一个重要的规则:非常值左值引用不能绑定右值。接下来,解释一下什么是左值与右值。

左值与右值

例:

int a = 114 + 514;

这条语句在内存的栈上开辟了一个4字节的空间,a有一个属于他的地址&a。而114 + 514是一个表达式字面量,他也有值,但是我们无法用一个地址获得它(它可能位于内存,也可能位于寄存器)。a位于等号左侧,属于左值;114 + 514位于等号右侧,属于右值。

百科中的定义:

  • L-value中的L指的是Location,表示可寻址。a value (computer science)that has an address.
  • R-value中的R指的是Read,表示可读。in computer science, a value that does not have an address in a computer language.

也就是说,区分左值还是右值的依据是:是否可以根据某个显示声明的变量获取到他的内存地址。有名字,可以取地址的就是左值,反之是右值(将亡值或纯右值)。
左值有地址,所以一定在内存中;右值则可能在寄存器中。

左值引用和右值引用

C++的引用就是一个值别名,他用于绑定一个值且不占空间。

  • 对一个左值进行引用的类型叫左值引用
  • 对一个右值进行引用的类型叫右值引用

左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。

引用类型还可以被const修饰,被const修饰的引用对绑定的值是只读的。根据引用类型左右值,和是否被const修饰,将引用类型分为四种(ra):

  • 常左值引用:const T& ra = a
  • 非常左值引用:T& ra = a
  • 常右值引用:const T&a = 1
  • 非常右值引用:T&& a = 1

其中非常左值引用非常右值引用可以对绑定的对象修改,而常左值引用常右值引用对绑定的对象只读。

常左值引用是万能引用,除了可以绑定左值和右值,可以和任意上述四种引用类型绑定,提供只读操作:

  1. 绑定左值

常量左值绑定:

const int a = 1;
const int& ra = a;

非常量左值绑定:

int a = 1;
const int& ra = a;

  1. 绑定右值

    const int& ra = 1 + 1;
  2. 绑定常左值引用

    int b = 1;
    const int& rb = b;
    const int& ra = rb;
  3. 绑定非常左值引用

    int b = 1;
    int& rb = b;
    const int& ra = rb;
  4. 绑定常右值引用

    const int& a = 1 + 1;
    const int& ra = a;
  5. 绑定非常右值引用

    int&& a = 1 + 1;
    const int& ra = a;

非常左值引用只能绑定非常左值引用,绑定右值则会失败:

int& a = 1 + 1; //编译错误

回到之前的问题:

friend std::ostream& operator << (std::ostream &out, matrix &m)

其中matrix& m参数是非常左值引用,当执行cout << m1 << endl时,m1是一个变量,属于左值,能够被非常左值引用绑定。而执行cout << m1 + m2 << endl的时候,m1 + m2是一个表达式,属于右值,而非常左值引用无法绑定右值,所以编译器会找不到参数列表匹配的函数,编译出错。

用关键字T&& a表示右值引用,右值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值,例如:

int a; 
int &&r1 = a; # 右值引用r1直接绑定左值a,编译失败
int &&r2 = std::move(a); # 强行把左值a转化成右值,再绑定到右值引用r2,编译通过

下面来说一下为什么要右值引用,右值引用在你需要使用寄存器中的值的时候可以进行右值引用。寄存器的刷新速度很快,没有右值引用的话就需要将寄存器中的值拷贝到内存中,在进行使用,这是很浪费时间的。

int getdata(int &&num)
{
cout << num;
num += 10;
return num;
}
void main()
{
int a = 5;
cout << getdata(a + 1) << endl;
}

如上int getdata(int &&num)就是对右值进行引用。 getdata(a + 1)a + 1是右值在寄存器中,我们是不可以直接对他进行操作的,如果要操作得将其拷贝到内存中,如果是一个非常大的数据这种拷贝就会很占用内存,如果直接用右值引用就可以直接对其进行操作。从而节约内存。将右值转化为左值 直接新建变量然后赋值就可以了

右值和左值可以相互转化:

int b = a + 1; //将 a + 1这个右值转变为左值了
move(a) //将a这个左值转变为了右值

参考资料

Disqus评论区没有正常加载,请使用科学上网