码农知识堂 - 1000bd
  •   Python
  •   PHP
  •   JS/TS
  •   JAVA
  •   C/C++
  •   C#
  •   GO
  •   Kotlin
  •   Swift
  • 右值引用,移动语义,完美转发


    文章预先发布于:pokpok.ink

    名词解释

    • 移动语义:用不那么昂贵的操作代替昂贵的复制操作,也使得只支持移动变得可能,比如 unique_ptr,将数据的所有权移交给别人而不是多者同时引用。

    • 完美转发:目标函数会收到转发函数完全相同类似的实参。

    • 右值引用:是这两个机制的底层语言机制,形式是 Type&&,能够引用到“不再使用”的数据,直接用于对象的构造

    要注意的是,形参一定是左值,即使类型是右值引用:

    void f(Widget&& w) {
        /* w 在作用域内就是一个左值。 */
    }
    

    实现移动语意和完美转发的重要工具就是std::move 和 std::forward,std::move 和 std::forward 其实都是强制类型转换函数,std::move 无条件将实参转换为右值,而 std::forward 根据实参的类型将参数类型转化为左值或者右值到目标函数。

    移动语义

    std::move(v) 相当于 static_cast(v),强制将类型转化为需要类型的右值,move 的具体实现为:

    template
    typename remove_reference::type&&
    move(T&& param) {
        using ReturnType = typename remove_reference::type&&;
        return static_cast(param);
    }
    
    1. 其中 typename remove_reference::type&& 作用就是为了解决是当入参数是 reference to lvalue 的时候,返回类型ReturnType会因为引用折叠被推导为 T&,remove_reference::type 就是为了去除推导出的模版参数 T 的引用,从到强制得到 T&&。

    2. 如果没有remove_reference,而是用 T&& 来代替函数返回值以及 static_cast<>,就会有下面的推导规则:

      • 如果入参是 lvalue,那么 T 就会被推导成为 T&,参数 param 的类型就变成了 T& &&,再通过引用折叠的规则,推导最终结果为 T&,而根据表达式的 value category 规则,如果一个函数的返回值类型是左值引用,那么返回值的类型为左值,那么 std::move(v) 就不能实现需要的功能,做到强制右值转换。
      • 如果入参是 rvalue,那么 T 会被直接推导成 T,参数 param 的类型也就变成了 T&&,函数返回的类型(type)也是 T&&,返回的值类别也是右值。
    3. 此外,在 c++14 能直接将 typename remove_reference::type&& 替换为 remove_reference_t&&。

    完美转发

    std::forward(v) 的使用场景用于函数需要转发不同左值或者右值的场景,假设有两个重载函数:

    void process(const Widget& lvalArg);
    void process(Widget&& rvalArg);
    

    有一个函数 LogAndProcess 会根据 param 左值或者右值的不同来区分调用不同函数签名的 process 函数:

    template
    void LogAndProcess(T&& param) {
        // DoSomething
        logging(param);
    
        process(std::forward(param));
    }
    

    这样使用 LogAndProcess 的时候就能区分:

    Widget w;
    LogAndProcess(w); // call process(const Widget&);
    LogAndProcess(std::move(w)); // call process(Widget&&);
    

    这里就有 emplace_back 一种常见的用错的情况,在代码中也经常看见,如果要将某个不用的对象a放到vector中:

    class A {
    	A(A&& a) {}
    };
    
    A a;
    std::vector vec;
    vec.push_back(a);
    

    如果使用 push_back 就会造成一次拷贝,常见的错误做法是将其替换为 vector::emplace_back():

    vec.emplace_back(a);
    

    但是 emplace_back 的实现有 std::forward 根据实参数做转发,实参 a 实际上是个 lvaue,转发到构造函数时得到的也是左值的 a,就相当于调用赋值拷贝构造:

    vec[back()] = a;
    

    解决方法其实只需要调用 std::move 做一次右值转换即可,就能完成数据的移动。

    vec.emplace_back(std::move(a)); 
    

    万能引用和右值引用

    万能引用和右值引用最大的区别在于万能引用会涉及模板的推导。但并不是说函数参数中有模板参数就是万能引用,例如 std::vector::push_back() 的函数签名是 push_back(T&& x), 但是 T 的类型在 std::vector 声明的时候就已经确定了,在调用push_back 的时候不会涉及类型推导,而 std::vector 的 emplace_back 是确实存在推导的。另外即使类型是 T&&,但是如果有 const 修饰得到 const T&&,那么也不是一个合格的万能引用。

    对于万能引用,如果是入参是右值引用,模版参数 T 的推导结果还是 T,而不是 T&&,这不是右值引用的特殊性,而是左值引用的特殊性,
    模板函数的函数参数列表中包含 forwarding reference 且相应的实参是一个 lvalue 的情况时,模版类型会被推导为左值引用。 forwarding reference 是 C++ 标准中的词,翻译叫转发引用;《modern effective c++》的作者 Scott Meyers 将这种引用称之为万能引用(universal reference)。

    右值引用的重载

    有了右值引用后,就能通过 std::move 将左值转换为右值,完成目标对象的移动构造,省去大对象的拷贝,但是如果传递的参数是个左值,调用者不希望入参被移动,数据被移走,那就需要提供一个左值引用的版本,因为右值引用无法绑定左值。假设大对象是一个string,就会写出下面这种函数签名:

    void f(const std::string& s);
    void f(string&& s);
    

    一个参数没问题,我们学会了左值和右值的区别并给出了不同的函数重载,但是如果参数是两个 string,情况就变得复杂的,针对不同的情况,就需要提供四种函数签名和实现:

    void f(const std::string& s1, const std::string& s2);
    void f(const std::string& s1, string&& s s2);
    void f(string&& s s1, const std::string& s2);
    void f(string&& s s1, string&& s s2);
    

    如果参数进一步增加,编码就会越来越复杂,遇到这种情况就可以使用万能引用处理,在函数体内对string做std::forward()即可:

    template
    void f(T1&& s1, T2&& s2);
    

    万能引用的重载

    万能引用存在一个问题,过于贪婪而导致调用的函数不一定是想要的那个,假设 f() 除了要处理左值和右值的 string 外,还有可能需要处理一个整形,例如int,就会有下面这种方式的重载。

    // 万能引用版本的 f(),处理左值右值
    template
    void f(T&& s) {
        std::cout << "f(T&&)" << std::endl;
    }
    
    // 整数类型版本的 f(),处理整形
    void f(int i)  {
        std::cout << "f(int)" << std::endl;
    }
    

    如果用不同的整型去调用f(),就会发生问题:

    int i1;
    f(i1); // output: f(int)
    
    size_t i2;
    f(i2); // output: f(T&&)
    
    • 如果参数是一个 int,那么一切正常,调用f(int)的版本,因为c++规定,如果一个常规函数和一个模板函数具备相同的匹配性,优先使用常规函数。
    • 但是如果入参是个 size_t,那么就出现问题了,size_t 的类型和 int 并不相等,需要做一些转换才能变成int,那么就会调用到万能引用的版本。

    如何限制万能引用呢?

    1、标签分派:根据万能引用推导的类型,f(T&&) 新增一个形参变成f(T&&, std::true_type)和f(T&&, std::false_type),调用f(args, std::is_integral()) 就能正确分发到不同的 f() 上。
    2、模板禁用:std::enable_if 能强制让编译器使得某种模板不存在一样,称之为禁用,底层机制是 SFINAE

    std::_enable_if 的正确使用方法为:

    templatestd::enable_if::type>
    void f(T param) {
    
    }
    

    应用到前面的例子上,希望整型只调用f(int)而 string 会调用 f(T&&),就会有:

    void f(int i) {
        std::cout << "f(int)" << std::endl;
    }
    
    templatestd::enable_if<
                std::is_same<
                    typename std::remove_reference::type, 
                    std::string>::value
                >::type
            >
    void f(T&& s) {
        std::cout << "f(T&&)" << std::endl;
    }
    

    模板的内容看上去比较长,其实只是在std::enable_if的condition内希望入参的类型为string,无论左值和右值,这样就完成了一个万能引用的正确重载。

    引用折叠

    在c++中,引用的引用是非法的,但是编译器可以推导出引用的引用的引用再进行折叠,通过这种机制实现移动语义和完美转发。

    模板参数T的推导规则有一点就是,如果传参是个左值,T的推导类型就是T&,如果传参是个右值,那么T推导结果就是T(不变)。引用的折叠规则也很简单,当编译器出现引用的引用后,结果会变成单个引用,在两个引用中,任意一个的推导结果为左值引用,结果就是左值引用,否则就是右值引用。

    返回值优化(RVO)

    编译器如果要在一个按值返回的函数省略局部对象的复制和移动,需要满足两个条件:

    1. 局部对象的类型和返回值类型相同
    2. 返回的就是局部对象本身

    如果在return的时候对局部变量做std::move(),那么就会使得局部变量的类型和返回值类型不匹配,原本可以只构造一次的操作,变成了需要构造一次加移动一次,限制了编译器的发挥。

    另外,如果不满足上面的条件二,按值返回的局部对象是不确定的,编译器也会将返回值当作右值处理,所以对于按值返回局部变量这种情况,并不需要实施std::move()。

  • 相关阅读:
    python-opencv划痕检测-续
    [WPF] 如何实现文字描边
    vue3:获取当前路由地址
    跳球过障碍游戏 C++
    owasp top 10
    四个小车相对导航集中式无迹卡尔曼滤波(fullyCN-EKF)
    【学习笔记】软件工程概述
    云服务器如何选?腾讯云2核2G3M云服务器88元一年!
    SpringBoot中使用Redisson分布式锁的应用场景-多线程、服务、节点秒杀/抢票处理
    TF-IDF(Term Frequency-Inverse Document Frequency)
  • 原文地址:https://www.cnblogs.com/pokpok/p/16163948.html
  • 最新文章
  • 攻防演习之三天拿下官网站群
    数据安全治理学习——前期安全规划和安全管理体系建设
    企业安全 | 企业内一次钓鱼演练准备过程
    内网渗透测试 | Kerberos协议及其部分攻击手法
    0day的产生 | 不懂代码的"代码审计"
    安装scrcpy-client模块av模块异常,环境问题解决方案
    leetcode hot100【LeetCode 279. 完全平方数】java实现
    OpenWrt下安装Mosquitto
    AnatoMask论文汇总
    【AI日记】24.11.01 LangChain、openai api和github copilot
  • 热门文章
  • 十款代码表白小特效 一个比一个浪漫 赶紧收藏起来吧!!!
    奉劝各位学弟学妹们,该打造你的技术影响力了!
    五年了,我在 CSDN 的两个一百万。
    Java俄罗斯方块,老程序员花了一个周末,连接中学年代!
    面试官都震惊,你这网络基础可以啊!
    你真的会用百度吗?我不信 — 那些不为人知的搜索引擎语法
    心情不好的时候,用 Python 画棵樱花树送给自己吧
    通宵一晚做出来的一款类似CS的第一人称射击游戏Demo!原来做游戏也不是很难,连憨憨学妹都学会了!
    13 万字 C 语言从入门到精通保姆级教程2021 年版
    10行代码集2000张美女图,Python爬虫120例,再上征途
Copyright © 2022 侵权请联系2656653265@qq.com    京ICP备2022015340号-1
正则表达式工具 cron表达式工具 密码生成工具

京公网安备 11010502049817号