Snail

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
统计
 

引用

A reference can be thought of as a name of an object.

  • 左值引用: 引用左值.

  • 右值引用: 引用右值, 用于参数传递, 函数返回值, 表达式中间结果. 类似于常量引用, 但是可以被修改.

(左值)引用类型的变量只能绑定到左值表达式, 只有const引用可以绑定到右值.
右值引用类型的变量只能绑定到右值表达式.

左值引用很好理解也经常用到, 右值引用还有点陌生, 接下来详细聊一聊.
引入右值引用是为了解决什么问题? 引入后又带来的什么新的问题? 新的问题又是怎么解决的?

引入右值引用是为了解决什么问题?

为了弥补C++在移动语义上的缺失. 在右值引用出现之前, 我们在函数调用传参时, 只有两种语义: 给它一份拷贝(按值传递), 或者给它一份引用(按引用传递).

那移动语义又是什么玩意儿? 为什么需要移动语义?

其实就是移动构造和移动赋值运算符, 把一个对象持有的资源转移给另一个新的对象, 避免先拷贝再释放资源的问题. 区别于已有的拷贝构造和赋值.

至于为什么需要, 无外乎是实践中有相应的使用场景.
比如std::unique_ptr, 它不能支持拷贝构造, 因为std::unique_ptr的语义就是要独占对象所有权.
这样一来函数要想返回一个std::unique_ptr就做不到了, 因为函数返回的时候要要构造一个新的std::unique_ptr, 自然就会涉及到拷贝构造.
有了移动语义就方便多了, 把原来的std::unique_ptr上的对象所有权转移到一个新的std::unique_ptr上并返回. 原来的std::unique_ptr随着函数结束自然销毁.

class unique_ptr {
public:

    unique_ptr(unique_ptr&& _Right) noexcept // 移动构造
    {
    }

    template <class _Dx2 = _Dx, enable_if_t<is_move_assignable_v<_Dx2>, int> = 0>
    unique_ptr& operator=(unique_ptr&& _Right) noexcept // 移动赋值
    {
        if (this != _STD addressof(_Right)) {
            reset(_Right.release());
            _Mypair._Get_first() = _STD forward<_Dx>(_Right._Mypair._Get_first());
        }
        return *this;
    }

    unique_ptr(const unique_ptr&) = delete;

    unique_ptr& operator=(const unique_ptr&) = delete;
};

引入后又带来的什么新的问题?

先看个例子, 下面代码中的foo(rref)会调到哪个实现foo会调到哪个重载?

void foo(int &)  { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }

int main() {
  int &&rref = 1;
  foo(rref);    // output: lvalue
}

答案是foo(int &), 是不是有点无法理解? 接着往下看.

foo(int &)接受一个左值类型的形参, 根据左值引用的规则, 左值引用类型的变量只能绑定左值. 所以, 要想匹配这个重载, 调用者必须传一个左值. 比如foo(a)

foo(int &&)接受的是一个右值类型的形参, 根据右值引用的规则, 右值引用类型的变量只能绑定右值表达式. 所以, 要想匹配这个重载, 调用者必须传一个右值, 比如foo(99).

这很好理解, 神奇的地方在rref这个变量. rref的类型是int&&(右值引用), 绑定的值的类别是右值, 但有意思的是, 这个变量本身却是一个左值.

所以, 按照上面的重载匹配规则, 自然就匹配到了foo(int &).

这也就是引入右值引用后带来的问题:
变量类型值类别, 变成了两个概念. 和左值引用表达式只能是左值不同, 右值引用类型的表达式既可以是左值也可以是右值.
重载匹配的时候右值引用类型的实参被当成了左值来用. 导致语义上出现错误.

新的问题又是怎么解决的?

标准库中提供了个std::move函数:

template <class _Ty>
constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{
    return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

其实就是一个类型转换, 把传入的实参_Arg转换成一个右值.

所以, 上面的foo应该这样调:

int main() {
  int &&rref = 1;
  foo(std::move(rref));
  }

完美转发

上面讲的是最基础的场景, 接下来再看看和模板结合的场景:

void foo(int &)
{
    std::cout << "lvalue" << std::endl;
}

void foo(int &&)
{
    std::cout << "rvalue" << std::endl;
}

template<typename T>
int bar(T&& x)
{
    foo(x);
}

从语义上看, 我们希望给bar传一个左值时能够转调到foo的左值引用版本(foo(int&)),
bar传一个右值时能够转调到foo的右值引用版本(foo(int&&)).

但是bar的形参x本身就是一个左值, 所以无论怎么传只能匹配到foo的左值引用版本, 显然达不到我们想要的目的.
这时候就需要完美转发了, 将函数实参以其原本的值类别转发出去. 标准库中提供了个std::forward函数.

所以上面的bar可以这么写:

template<typename T>
int bar(T&& x)
{
    foo(std::forward<T>(x));
}

扒一扒std::forward源码:

template <class _Ty>
constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{
    return static_cast<_Ty&&>(_Arg);
}

template <class _Ty>
constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept
{
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call"); // _Ty不能是左值引用类型
    return static_cast<_Ty&&>(_Arg);
}

其实就是一个静态类型转换, 传入左值的时候, 会匹配第一个重载, 这个重载根据_Ty的类型将实参转发为左值或右值. 传入右值时, 会匹配第二个重载, 这个重载根据_Ty的类型将实参转发为右值.
需要注意的是模板参数_Ty无法由编译器推断, 必须显式指定.

第一个重载不是很好理解, 补充一下展开过程:

  • 模板参数传入int展开:
int && forward(int& _Arg) 
{
    return static_cast<int &&>(_Arg); // 按右值转发
}
  • 模板参数传入int&展开:
int& && forward(int& _Arg) 
{
    return static_cast<int& &&>(_Arg); // 引用折叠, 折叠为int&, 按左值转发
}
  • 模板参数传入int&&展开:
int&& && forward(int& _Arg)
{
    return static_cast<int&& &&>(_Arg); // 引用折叠, 折叠为int&&, 按右值转发
}

这里其实涉及到引用折叠转发引用两个概念, 再补充说明一下:

o 引用折叠:

模板编程中参数类型推导出现双重引用时, 双重引用将被折叠成一个引用, 要么是左值引用, 要么是右值引用.
折叠规则是: 如果任一引用为左值引用, 则结果为左值引用, 否则(即两个都是右值引用), 结果为右值引用.

rvalue reference to rvalue reference collapses to rvalue reference, all other combinations form lvalue reference.

o 转发引用:
转发引用就是对一个待推导的类型 T 应用上右值引用的形式(T&&)。编译器在做类型推导时,会将绑定左值的T推导为左值引用类型,将绑定右值的T推导为原基本类型,再叠加引用坍缩规则后,绑定左值的变量的实际类型为左值引用,绑定右值的变量的实际类型为右值引用。

参考文献:

Everything about Rvalue Reference: https://www.zhihu.com/question/363686723/answer/1976488046
c++11的移动语义和完美转发: https://zhuanlan.zhihu.com/p/398817111

posted on   Snail-0304  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
 
点击右上角即可分享
微信分享提示