- C左值右值
- 左值和右值的由来
- 什么是左值和右值
- 左值右值的本质
- 引用
- 左值引用
- 右值引用
- 移动语句与完美转发
- 移动语句
- 实现移动构造函数和转移赋值函数
- stdmove
- 完美转发Perfect Forwarding
- 移动语句
C++左值右值
自从C++11发布之后,出现了一个新的概念,即左值和右值,英文为lvalue和rvalue,这两个是比较晦涩难懂的基础概念,为什么说是基础的概念呢?因为只有了解了它,才能真正理解move和forward语句。
左值和右值的由来
左值和右值,最早是从C语言继承而来的。在C语言中,或者是它的继承版本中有如下变现形式:
- 左值是可以位于赋值运算符“=”左侧的变量或表达式,也可以位于赋值运算符“=”右侧
- 右值是不可以位于赋值运算符“=”左侧的表达式,只能出现在等号右边的变量或者表达式
我们来看看例子:
例子1:
int a; //声明变量a
int b; //声明变量ba = 3; //赋值语句,将a的值重新赋值为3,此时a为左值
b = 4; //此时b为左值a = b; //此时a为左值,b为右值
b = a;//此时b为左值,a为右值3 = a; //编译错误,这里应该很好理解,3不可能再被赋值了
a + b = 4; //编译错误,这里相当于7 = 4,也是不合理的
例子2
//定义了两个函数foo1和foo2
int foo1(int number)
{return number;
}int foo2( int number )
{return number;
}main()
{foo1(1) = foo2(2); //编译错误,因为foo1和foo2的返回值只能作为右值,不能放在等号的左边int temp = foo1(1) * foo2(2); //temp为左值,foo1(1) * foo2(2)为右值
}
上面这些例子就是左值和右值的例子。
什么是左值和右值
一个变量或者表达式是左值还是右值,取决于我们使用的是它的值还是它在内存中的位置(作为实例的身份)。
int a; //声明变量a
int b; //声明变量ba = b; //此时a为左值,b为右值
这个例子中,将b的值赋值给a,将值保存在a的内存中,b在这里面是右值,a在这里面是左值
因为b作为实例既可以当做左值也可以当做右值。
所以判断一个值是左值还是右值要根据实际在语句汇总的含义来确定。
总结第一点:
- 在一般情况下,需要右值的地方可以用左值来代替,需要左值的地方必须使用左值
- 左值存放在实例中,有持久的状态,而右值是字面常量,要么是在表达式求值过程中创建的临时实例,没有持久的状态
重点:
能取得到地址的变量或者表达式就是左值,反之为右值。
那现在我们来看看下面的例子哪个是左值,哪个是右值?
int a = 0;
int b = 1;
a++;
++b;
我来宣布答案,a++为右值,++b为左值,首先我们先验证一下:
int a = 0;
int b = 1;
a++ = 5; //error: lvalue required as left operand of assignment
++b = 5;
实验的结果也是正确的,那我们来分析一下:
对于a++
1. a++首先产生一个临时变量,记录a的值
2. 然后将a+1
3. 接着返回临时变量
根据这个过程我们知道 int a = 0;int c = a++; 的值应该是c为0;而a变为了1,
所以a++此时将临时变量返回给了c,那么这个临时变量我们是不能获取地址的,也就
是使用“&”。所以结论就是a++为右值。
对于++b
1. 进行了b = b + 1
2. 返回变量b
根据这个过程,我们是可以取b的地址的,所以b是左值。
左值右值的本质
int a = 5;
int c = a + a;
a就是左值,5就是右值。 a + a 表达式中,a以右值传入,相加之后也以右值返回。
左值就是对一块内存区域的引用(这个并不是c++11中的int &a 之类的引用),
比如上边的a,就对应了一块内存区域(起始地址&a,大小为sizeof(int) )。
专业的解释:
An object is a region of storage that can be examined and stored into. An lvalue is an expression that refers to such an object. An lvalue does not necessarily permit modification of the object it designates. For example, a const object is an lvalue that cannot be modified.
对于每个变量,都有2个值与其相关联:
- 数据值,存储在某个内存地址中,也称为右值,右值是被读取的值,不可修改。
- 地址值,即存储数据值的那块内存地址,也称左值。
所以左值既可以当作左值也可以作为右值。就是这么神奇。
引用
在C++中,有两种对实例的引用:左值引用和右值引用。
左值引用
左值引用是常见的引用,C++中可以使用“&”符号定义引用,如果一个左值同时也是引用,那么就称其为“左值引用”。如:
std::string str;
std::string& strRef = str; // strRef为左值也为引用,称其为左值引用
非const左值引用不能使用右值对其赋值
std::string& strRef = "abc"; // error: abc字符串为右值,
假设上面可以的话,就会遇到一个问题:如何修改右值的值?因为引用是可以后续被赋值的。根据上面的定义,右值连可被获取的内存地址都没有,也就谈不上对其进行赋值。
但是const左值引用就可以使用右值,因为常量不能被修改,也不存在上面纠结的问题:
const std::string strRef = "abc";
再比如,我们经常使用左值作为函数的参数类型,可以减少不必要的对象复制:
int foo( int& number )
{return number;
}main()
{int a = foo(1); //错误int b = 1;int c = foo(b); //通过
}
我们将上面的int& number改为 const int& number即可。
补充知识:
什么是CV限定符(CV-qualified),如果变量声明时类型带有const或者volatile,就说此变量类型具有CV限定符。
右值引用
右值引用也是引用,但是它只能且必须绑定在右值上。
int a = 5;
int& b = a; // a绑定在左值引用b上
int&& c = a; // error:a可以是左值,所以不能将它绑定在右值引用上。
int&& d = 30; // 将右值30绑定在右值引用上
int&& e = a * 1 // a * 1的结果是一个临时对象,为右值,所以可以绑定在右值引用上
结论:
由于右值引用只能绑定在右值上,而右值要么是字面常量,要么是临时对象,所以:
右值引用的对象,是临时的,即将被销毁; 并且右值引用的对象,不会在其它地方使用。
移动语句与完美转发
这里涉及前面提过的move和forward两个函数,即移动语句和转发。
右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
- 能够更简洁明确地定义泛型函数。
移动语句
右值引用是用来支持移动语句的。移动语句可以将资源(堆,系统对象等)从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高C++应用程序的性能。临时对象的维护(创建和销毁)对性能有严重影响。
移动语句是和拷贝语句相对的,可以类比文件的剪切和拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。
通过移动语句,临时对象中的资源能够转移其他的对象里。
在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现移动语句,需要定义移动构造函数,还可以定义移动赋值操作符。对于右值的拷贝和赋值会调用移动构造函数和移动赋值操作符。如果移动构造函数和移动拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。
实现移动构造函数和转移赋值函数
class MyString { public: MyString() { m_data = nullptr; m_len = 0; }private: char* m_data; size_t m_len; void initData(const char *s) { m_data = new char[m_len+1]; memcpy(m_data, s, m_len); m_data[m_len] = '\0'; } MyString(const char* p) { m_len = strlen (p); initData(p); } MyString(const MyString& str) { m_len = str.m_len; initData(str.m_data); std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl; } MyString& operator=(const MyString& str) { if (this != &str) { m_len = str.m_len; initData(str.m_data); } std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl; return *this; } virtual ~MyString() { } }; int main()
{ MyString a; a = MyString("Hello"); //调用拷贝构造函数,MyString("Hello")为临时对象,即右值std::vector<MyString> vec; vec.push_back(MyString("World"));
}运行结果:
Copy Assignment is called! source: Hello
Copy Constructor is called! source: World
这个 string 类已经基本满足我们演示的需要。在 main函数中,实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。
MyString(“Hello”) 和 MyString(“World”)都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。
MyString(MyString&& str)
{ std::cout << "Move Constructor is called! source: " << str.m_data << std::endl; m_len = str.m_len; m_data = str.m_data; str.m_len = 0; str.m_data = nullptr;
}MyString& operator=(MyString&& str)
{ std::cout << "Move Assignment is called! source: " << str.m_data << std::endl; if (this != &str) { m_len = str.m_len; m_data = str.m_data; str.m_len = 0; str.m_data = nullptr; } return *this;
}增加后的运行结果:Move Assignment is called! source: Hello
Move Constructor is called! source: World
由此看出,编译器区分了左值和右值,对右值调用了移动构造函数和移动赋值操作符。节省了资源,提高了程序运行的效率。
有了右值引用和移动语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计移动构造函数和转移赋值函数,以提高应用程序的效率。
std::move
我们先来看一个例子:
void processValue( int& value )
{std::cout << "lvalue process: " << value << std::endl;
}void processValue( int&& value )
{std::cout << "rvalue process: " << value << std::endl;
}main()
{int a = 0;processValue(a); //传入左值processValue(1); //传如右值
}结果:
lvalue process: 0
rvalue process: 1
通过这个例子来看,processValue函数被重载,分别接受左值和右值。由输出结果可以看出,临时对象是作为右值处理的。
但是如果临时对象通过一个接受右值的函数传递给另一个函数时,就会变成左值,因为这个临时对象在传递过程中,变成了命名对象。
void processValue( int& value )
{std::cout << "lvalue process: " << value << std::endl;
}void processValue( int&& value )
{std::cout << "rvalue process: " << value << std::endl;
}void forwardValue( int&& value )
{processValue(value);
}main()
{int a = 0;processValue(a);processValue(1);forwardValue(2);
}结果:
lvalue process: 0
rvalue process: 1
lvalue process: 2
我们可以看出最后一个函数调用,2是右值,可以返回的时候却变成了左值。这里面我们可以使用std::move(var)将变量转移为右值语句。
修改为:
...
void forwardValue( int&& value )
{processValue(std::move(value) );
}
...
既然编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。
std::move在提高 swap 函数的的性能上非常有帮助,一般来说,swap函数的通用定义如下:
template <class T> swap(T& a, T& b)
{ T tmp(a); // copy a to tmp a = b; // copy b to a b = tmp; // copy tmp to b
}
有了 std::move,swap 函数的定义变为 :
template <class T> swap(T& a, T& b)
{ T tmp(std::move(a)); // move a to tmp a = std::move(b); // move b to a b = std::move(tmp); // move tmp to b
}
通过 std::move,一个简单的 swap 函数就避免了 3 次不必要的拷贝操作。
完美转发(Perfect Forwarding)
Perfect Forwarding也被翻译成完美转发,精准转发等,说的都是一个意思。
用于这样的场景:需要将一组参数原封不动的传递给另一个函数。
原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:
- 左值/右值
- const/non-const。
完美转发就是在参数传递过程中,所有这些属性和参数值都不能改变。
重点:完美转发仅与类模板或函数模板的环境有关。
示例:
class Person
{
public:template<typename T1, typename T2>Person(T1&& first, T2&& second ) : firstname{std::forward<T1>(first)},secondname{std::forward<T2>(second)}{}string getName() const{return firstname.getName() + " " + secondname.getName(); }
private:Name firstname;Name secondname;
}class Name
{
public:Name( const string& aName ): name{aName}{cout << "Lvalue Name constructor." << endl;}Name( string&& aName ): name{std::move(aName)}{cout << "Rvalue Name constructor." << endl;}const string& getName() const { return name; }
private:string name;
}main()
{Person me{string{"abc"}, string{"def"}};string first{"lll"};string second{"ggg"};Person other{first, second};
}输出结果:
Rvalue Name constructor.
Rvalue Name constructor.
Lvalue Name constructor.
Lvalue Name constructor.