C++11/14/17 学习的第一篇:左值、右值、左值引用、右值引用、移动语义、完美转发。
左值和右值
- 左值是可以放在赋值号左边可以被赋值的值,它必须要在内存中有实体。
- 右值当在赋值号右边取出值赋给其他变量的值,它可以在内存也可以在寄存器。
总的来讲,左值可以被赋值或者修改,而右值不能。
下面是一些例子:
int a, b, *p;
const int c = 7;
// 【正确】a 为左值,7 为右值
a = 7;
// 【错误】7 为常量,故为右值。右值不能被赋值。
7 = a;
// 【错误】j * 4 为表达式,故为右值。右值不能被赋值。
b * 4 = 7;
// 【正确】*p 为取址,是左值。
*p = a;
// 【正确】等号左边的表达式返回的是左值。
((a < 3) ? a : b) = 7;
// 【错误】ci 为常量,故为右值。右值不能被赋值。
c = 9;
左值引用右值引用
- 左值引用相当于变量的别名。当它引用另一个变量时,指向的还是同一个地址。
- 右值引用与左值引用类似,不过它引用的是一个右值。
总的来讲,左值引用只能引用左值,右值引用只能引用右值。
下面是一些例子:
int a = 100;
// 【正确】a 为左值,b 可以对其左值引用
int& b = a;
// 【错误】a++ 为右值,无法对其左值引用
int& c = a++; // error
// 【正确】200 为右值,d 可以对其右值引用
int&& d = 100;
// 【错误】a 为左值,无法对其右值引用
int&& e = a; // error
// 【正确】这是特例。由于左侧为常量,故可以对右值进行左值引用
const int& f = 100;
左值可以通过转换来被右值引用。
下面是一些例子:
int a = 1000;
// 使用 std::move 转为右值引用
int&& b = std::move(a);
// 使用 static_cast 转为右值引用
int&& c = static_cast<int&&>(a);
// 使用 C 风格强转为右值引用
int&& d = (int&&)a;
// 使用 std::forwad<T&&> 为右值引用
int&& e = std::forward<int&&>(a);
移动语义
我们知道,对于 C++ 中的一个类,如果类中有类似指针的东西,那么在默认情况下,拷贝构造时仅仅复制了指针指向的地址,而不是新开辟一块内存存放指针指向的内容,这称为“浅拷贝”。与之相对的就是“深拷贝”。
浅拷贝会使多个实例公用同一个对象,造成安全性问题;而深拷贝尽管安全,却造成了巨大的资源开销。
举个例子,小明和小红看电视。浅拷贝就是给了每人一个遥控器,两个人分别遥控电视时会产生冲突;深拷贝就是买了两台电视,各看各的,显得很浪费。最好的解决方法是一台电视一部遥控器,谁要看就把遥控器交给谁,这就是移动拷贝。
移动拷贝构造会使用移动语义,转移资源,相当于移交了资源的控制权。
下面是一个例子:
class class_a
{
public:
// 深拷贝构造函数
class_a(const class_a& b):
m_size(b.m_size)
{
m_data = new char[m_size];
memcpy(m_data, b.m_data, m_size);
}
// 移动拷贝构造函数
class_a(class_a&& b):
m_size(b.m_size),
m_data(b.m_data)
{
b.m_data =nullptr;
}
int m_size;
char* m_data;
};
// class_a(1000) 为右值,发移动拷贝构造
class_a a(class_a(1000));
move
move
可以将左值转换为右值引用。
例如对于上一节的类,我们有如下使用移动拷贝构造的方法:
class_a a(1000);
// a 为左值,通过 std::move(a) 转换为右值引用,发移动拷贝构造
class_a b(std::move(a));
move 的底层实现如下:
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept {
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
这是利用了引用的折叠规则
-
T& &
、T& &&
、T&& &
折叠成T&
-
T&& &&
折叠成T&&
所以首先去掉所有引用,在加上右值引用,就实现了强制转换为右值引用的功能。
forward
我们有一个例子:
void func(class_b& rA) {
// 左值引用版本函数
std::cout << "lvalue " std::endl;
}
void func(class_b&& rA) {
// 右值引用版本函数
std::cout << "rvalue " std::endl;
}
// class_b(1000) 为右值,调用右值引用版本函数
func(class_b(1000));
class_b&& a = class_b(1000);
// a 为右值引用,但本身是左值,调用左值引用版本函数
func(a);
我们希望能找到一个方法,使得传入函数的为左引用时,函数内保持左引用;传入函数的为右引用时,函数内保持右引用。
我们首先要使函数既能接收左值引用,也能接收右值引用。万能引用恰好可以实现这一想法:
template<typename T>
void func(T&& a) {
};
然后我们使用 forward
保持原来的引用关系:
template<typename T>
void func(T&& a){
b = std::forward<T>(a);
}
forward
能够完美转发以下几种类型:
T&
T&&
const T&
const T&&
forward
的底层实现如下:
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(
remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
return static_cast<_Ty&&>(_Arg);
}
上面的实现将左值转换为左值或者右值引,下面的实现将右值转换为右值。
move
和 forward
都可以被 static_cast
替代,但前者会在编译期间进行类型检查,更加安全。
总结
- 浅拷贝存在安全性问题,深拷贝临时变量开销太大。
- 所以使用右值引用传递临时变量。
- 使用
move
将左值变为右值引用。 - 使用
forward
同时处理左值和右值引用。
Comments