Ch3nyang's blog

home

home

person

about

hive

project

collections_bookmark

mindclip

rss_feed

rss

右值的应用

calendar_month 2021-07
archive 编程
tag cpp tag move

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);
}

上面的实现将左值转换为左值或者右值引,下面的实现将右值转换为右值。

moveforward 都可以被 static_cast 替代,但前者会在编译期间进行类型检查,更加安全。

总结

  • 浅拷贝存在安全性问题,深拷贝临时变量开销太大。
  • 所以使用右值引用传递临时变量。
  • 使用 move 将左值变为右值引用。
  • 使用 forward 同时处理左值和右值引用。

参考资料

Comments

Share This Post