• c++新特性 语言运行期强化


    目录

    1.Lamabad

    1.1基础语法

    1.1.1值捕获

    1.1.2引用捕获

    1.1.3. 隐式捕获

    1.1.4表达式捕获

    2.函数对象包装器

    2.1.1std::function的来历

    2.1.2std::function的作用

    2.2std::bind和std::palceholder

    3 右值引用

    3.1 右值引用的来历

    3.2左值、右值的纯右值、将亡值、右值

    3.3右值引用和左值引用

    3.4移动语义

    3.5完美转发


    1.Lamabad

    Lambda 表达式是现代 C++ 中最重要的特性之一,而 Lambda 表达式,实际上就是提供了一个类
    似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用 的。这样的场景其实有很多很多,所以匿名函数几乎是现代编程语言的标配。

    1.1基础语法

    Lambda 表达式的基本语法如下:
    [ 捕获列表 ]( 参数列表 ) mutable( 可选 ) 异常属性 -> 返回类型 {
    // 函数体
    }
    上面的语法规则除了 [ 捕获列表 ] 内的东西外,其他部分都很好理解, 只是一般函数的函数名被略
    去,返回值使用了一个 -> 的形式进行
    所谓捕获列表 ,其实可以理解为参数的一种类型 lambda 表达式内部函数体在默认情况下是不能够 使用函数体外部的变量的 ,这时候捕获列表可以起到传递外部数据的作用。根据传递的行为,捕获列表 也分为以下几种:

    1.1.1值捕获

    与参数传值类似, 值捕获的前提是变量可以拷贝
    不同之处则在于, 被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝
    1. void lamada_value_capture(){
    2. int value=1;
    3. auto copy_value=[value]{
    4. return value;
    5. }
    6. value=100;
    7. auto stored_value = copy_value();
    8. std::cout << "stored_value = " << stored_value << std::endl;
    9. // 这时, stored_value == 1, 而 value == 100.
    10. // 因为 copy_value 在创建时就保存了一份 value 的拷贝
    11. }

    1.1.2引用捕获

    与引用传参类似,引用捕获保存的是引用,值会发生变化

    1. oid lamada_value_capture(){
    2. int value=1;
    3. auto copy_value=[&value]{
    4. return value;
    5. }
    6. value=100;
    7. auto stored_value = copy_value();
    8. std::cout << "stored_value = " << stored_value << std::endl;
    9. // 这时, stored_value == 100, value == 100.
    10. // 因为 copy_value 保存的是引用
    11. }

    1.1.3. 隐式捕获

    手动书写捕获列表有时候是非常复杂的, 这种机械性的工作可以交给编译器来处理 ,这时候可以在
    捕获列表中写一个 & = 向编译器声明采用引用捕获或者值捕获 .
    总结一下,捕获提供了 lambda 表达式对外部值进行使用的功能,捕获列表的最常用的四种形式可
    以是
    • [] 空捕获列表
    • [name1, name2, . . . ] 捕获一系列变量
    • [&] 引用捕获 , 让编译器自行推导捕获列表
    • [=] 值捕获 , 让编译器执行推导引用列表

    1.1.4表达式捕获

    上面提到的值捕获、 引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左
    值,而不能捕获右值。
    C++14 给与了我们方便,允许捕获的成员用任意的表达式进行初始化,这就允许了右值的捕获,被 声明的捕获变量类型会根据表达式进行判断, 判断方式与使用 auto 本质上是相同的
    1. #include
    2. #include
    3. int main()
    4. {
    5. auto importance=std::make_unique<int>(1);
    6. auto add=[[v1 = 1, v2 = std::move(important)](int x,int y)->int{
    7. return x+y+v1+(*v2);
    8. }
    9. std::cout << add(3,4) << std::endl;
    10. }

    1.2.泛型Lambda

    上一节中我们提到了 auto 关键字不能够用在参数表里,这是因为这样的写法会与模板的功能产生
    冲突。但是 Lambda 表达式并不是普通函数,所以 Lambda 表达式并不能够模板化。这就为我们造成了 一定程度上的麻烦:参数表不能够泛化,必须明确参数表类型。 幸运的是,这种麻烦只存在于 C++11 中, 从 C++14 开始,Lambda 函数的形式参数可以使用 auto
    关键字来产生意义上的泛型:
    1. auto add=[](auto x,auto y)
    2. {
    3. return x+y
    4. }
    5. add(1,2);
    6. add(1.2,3.2);

    2.函数对象包装器

    这部分内容虽然属于标准库的一部分,但是从本质上来看,它却增强了 C++ 语言运行时的能力,这 部分内容也相当重要,所以放到这里来进行介绍:

    2.1.1std::function的来历

     
    Lambda 表达式的本质是一个和函数对象类型相似的类类型(称为闭包类型)的对象(称为闭包对
    象),当 Lambda 表达式的捕获列表为空时,闭包对象还能够转换为函数指针值进行传递
    1. #include
    2. using foo=void(int);
    3. void function(foo f)//定义在参数列表中的函数类型 foo 被视为退化后的函数指针类型 foo*
    4. {
    5. foo(1);// 通过函数指针调用函数
    6. }
    7. int main()
    8. {
    9. auto f=[](int value)
    10. {
    11. std::cout << value << std::endl;
    12. }
    13. function(f);// 传递闭包对象,隐式转换为 foo* 类型的函数指针值
    14. f(1);// lambda 表达式调用
    15. }
    上面的代码给出了两种不同的调用形式:
    一种是将 Lambda 作为函数类型传递进行调用
    而另一种 则是直接调用 Lambda 表达式
    C++11 中,统一了这些概念,将能够被调用的对象的类型,统一称 之为可调用类型。而这种类型,便是通过 std::function 引入的。

    2.1.2std::function的作用

    C++11 std::function 是一种通用、多态的函数封装,它的实例可以对任何可以调用的目标实体进
    行存储、复制和调用操作,它也是对 C++ 中现有的可调用实体的一种类型安全的包裹(相对来说,函数 指针的调用不是类型安全的),换句话说,就是函数的容器。当我们有了函数的容器之后便能够更加方便 的将函数、函数指针作为对象进行处理。
    1. #include
    2. #include
    3. int foo(int para)
    4. {
    5. return para;
    6. }
    7. int main()
    8. {
    9. //std::function包装了一个返回值为 int, 参数为 int 的函数
    10. std::function<int(int)> fun=foo;
    11. int importance=10;
    12. std::function<int(int)> func2=[&](int value)->int
    13. {
    14. return 1+value+importance;
    15. }
    16. std::cout << func(10) << std::endl;
    17. std::cout << func2(10) << std::endl;
    18. }

    2.2std::bind和std::palceholder

    std::bind 则是用来绑定函数调用的参数的, 它解决的需求是我们有时候可能并不一定能够一次
    性获得调用某个函数的全部参数 ,通过这个函数,我们可以将部分调用参数提前绑定到函数身上成为一 个新的对象,然后在参数齐全后,完成调用。例如
    1. #include
    2. #include
    3. using namespace std;
    4. int TestFunc(int a, char c, float f)
    5. {
    6. cout << a << endl;
    7. cout << c << endl;
    8. cout << f << endl;
    9. return a;
    10. }
    11. int main()
    12. {
    13. auto bindFunc1 = bind(TestFunc, std::placeholders::_1, 'A', 100.1);
    14. bindFunc1(10); //等于TestFunc(10,'A', 100.1)
    15. cout << "=================================\n";
    16. auto bindFunc2 = bind(TestFunc, std::placeholders::_2, std::placeholders::_1, 100.1);
    17. bindFunc2('B', 10); //等于TestFunc(10,'B', 100.1)
    18. cout << "=================================\n";
    19. auto bindFunc3 = bind(TestFunc, std::placeholders::_2, std::placeholders::_3, std::placeholders::_1);
    20. bindFunc3(100.1, 30, 'C'); //等于TestFunc(30,'C', 100.1)
    21. return 0;
    22. }

    3 右值引用

    3.1 右值引用的来历

    右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大
    量的历史遗留问题 ,消除了诸如 std::vector、std::string 之类的额外开销 ,也才使得函数对象容器 std::function 成为了可能

    3.2左值、右值的纯右值、将亡值、右值

    左值 (lvalue, left value) ,顾名思义就是 赋值符号左边的值 。准确来说,左值是表达式(不一定是
    赋值表达式)后 依然存在的持久对象
    右值 (rvalue, right value) ,右边的值,是指表达式结束后就 不再存在的临时对象
    纯右值 (prvalue, pure rvalue) ,纯粹的右值,要么是纯粹的字面量,例如 10 , true ;要么是求值
    结果相当于字面量或匿名临时对象,例如 1+2 。非引用返回的临时变量、运算表达式产生的临时变量、原 始字面量、Lambda 表达式都属于纯右值
    需要注意的是, 字符串字面量只有在类中才是右值,当其位于普通函数中是左值
    1. class Foo {
    2. const char*&& right = "this is a rvalue"; // 此处字符串字面量为右值
    3. public:
    4. void bar() {
    5. right = "still rvalue"; // 此处字符串字面量为右值
    6. }
    7. };
    8. int main() {
    9. const char* const &left = "this is an lvalue"; // 此处字符串字面量为左值
    10. }
    将亡值 (xvalue, expiring value) ,是 C++11 为了引入右值引用而提出的概念(因此在传统 C++
    中,纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。
    1. std::vector<int> foo() {
    2. std::vector<int> temp = {1, 2, 3, 4};
    3. return temp;
    4. }
    5. std::vector<int> v = foo();
    在这样的代码中,就传统的理解而言,函数 foo 的返回值 temp 在内部创建然后被赋值给 v ,然而 v 获得这个对象时, 会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大,这将造成大量 额外的开销(这也就是传统 C++ 一直被诟病的问题) 。在最后一行中, v 是左值、 foo() 返回的值就是右值(也是纯右值)

    3.3右值引用和左值引用

    要拿到一个将亡值,就需要用到右值引用: T && ,其中 T 是类型。右值引用的声明让这个临时值的 生命周期得以延长、只要变量还活着,那么将亡值将继续存活。
    C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值,有了它我们就能够方便的获
    得一个右值临时对象,例如:
    1. #include
    2. #include
    3. void reference(std::string& str) {
    4. std::cout << " 左值" << std::endl;
    5. }
    6. void reference(std::string&& str) {
    7. std::cout << " 右值" << std::endl;
    8. }
    9. int main()
    10. {
    11. std::string lv1 = "string,"; // lv1 是一个左值
    12. // std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
    13. std::string&& rv1 = std::move(lv1); // 合法, std::move 可以将左值转移为右值
    14. std::cout << rv1 << std::endl; // string,
    15. const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
    16. // lv2 += "Test"; // 非法, 常量引用无法被修改
    17. std::cout << lv2 << std::endl; // string,string
    18. std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
    19. rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
    20. std::cout << rv2 << std::endl; // string,string,string,Test
    21. reference(rv2); // 输出左值
    22. return 0; }

    3.4移动语义

    传统 C++ 通过拷贝构造函数和赋值操作符为类对象设计了拷贝 / 复制的概念,但为了实现对资源的
    移动操作,调用者必须使用先复制、再析构的方式,否则就需要自己实现移动对象的接口。试想,搬家的 时候是把家里的东西直接搬到新家去,而不是将所有东西复制一份(重买)再放到新家、再把原来的东 西全部扔掉(销毁),这是非常反人类的一件事情。 传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。右值
    引用的出现恰好就解决了这两个概念的混淆问题,例如:
    1. #include
    2. class A
    3. {
    4. public:
    5. int* pointer;
    6. A():pointer(new int(1))
    7. {
    8. std::cout<<" 构造"<
    9. }
    10. A(A&a):pointer(new int(*a.pointer))
    11. {
    12. std::cout << " 拷贝" << pointer << std::endl;
    13. }
    14. A(A&& a):pointer(a.pointer) {
    15. a.pointer = nullptr;
    16. std::cout << " 移动" << pointer << std::endl;
    17. }
    18. ~A()
    19. {
    20. std::cout << " 析构" << pointer << std::endl;
    21. delete pointer;
    22. }
    23. };
    24. A return_rvalue(bool test)
    25. {
    26. A a,b;
    27. if(test )return a;
    28. else return b;
    29. //static_cast(b);

    代码结果:

    构造0x603010//a
    构造0x603030//b
    移动0x603030//b
    析构0//b
    析构0x603010//a
    obj:
    0x603030//b
    1
     析构0x603030

    分析:

    1. 首先会在 return_rvalue 内部构造两个 A 对象,于是获得两个构造函数的输出;
    2. 函数返回后,产生一个将亡值,被 A 的移动构造( A(A&&) )引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了 obj 中,而 将亡值的指针被设置为 nullptr ,防止了这块内存区域被销毁

    3.5完美转发

    前面我们提到了,一个声明的右值引用其实是一个左值。这就为我们进行参数转发(传递)造成了
    问题:
    1. #include
    2. void reference(int& v) {
    3. std::cout << " 左值" << std::endl;
    4. }
    5. void reference(int&& v) {
    6. std::cout << " 右值" << std::endl;
    7. }
    8. template <typename T>
    9. void pass(T&& v) {
    10. std::cout << " 普通传参:";
    11. reference(v); // 始终调用 reference(int&)
    12. }
    13. int main() {
    14. std::cout << " 传递右值:" << std::endl;
    15. pass(1); // 1 是右值, 但输出是左值
    16. std::cout << " 传递左值:" << std::endl;
    17. int l = 1;
    18. pass(l); // l 是左值, 输出左值
    19. return 0; }

    传递右值:
    普通传参: 左值
    传递左值:
    普通传参: 左值

    分析:对于 pass(1) 来说,虽然传递的是右值,但由于 v 是一个引用,所以同时也是左值。因此
    reference(v) 会调用 reference(int&) ,输出『左值』。而对于 pass(l) 而言, l 是一个左值,为什么
    会成功传递给 pass(T&&) 呢?
    这是基于 引用坍缩规则 的:在传统 C++ 中,我们不能够对一个引用类型继续进行引用,但 C++
    于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用,既能左引 用,又能右引用。但是却遵循如下规则:
    函数形式参数实参推导
    T&
    左引用
    T&
    T&
    右引用
    T&
    T&&
    左引用
    T&
    T&&
    右引用
    T& &

    总结 两者都右才是右

    完美转发就是基于上述规律产生的。所谓完美转发,就是为了让我们在传递参数的时候,保持原来
    的参数类型(左引用保持左引用,右引用保持右引用)。为了解决这个问题,我们应该使用 std::forward 来进行参数的转发(传递):
    1. #include
    2. void reference(int& v) {
    3. std::cout << " 左值" << std::endl;
    4. }
    5. void reference(int&& v) {
    6. std::cout << " 右值" << std::endl;
    7. }
    8. template <typename T>
    9. void pass(T&& v) {
    10. std::cout << " 普通传参:";
    11. reference(v); // 始终调用 reference(int&)
    12. std::cout << " std::move 传参: ";
    13. reference(std::move(v));
    14. std::cout << " std::forward 传参: ";
    15. reference(std::forward(v));
    16. std::cout << "static_cast 传参: ";
    17. reference(static_cast(v));
    18. }
    19. int main() {
    20. std::cout << " 传递右值:" << std::endl;
    21. pass(1); // 1 是右值, 但输出是左值(临时变量)
    22. std::cout << " 传递左值:" << std::endl;
    23. int l = 1;
    24. pass(l); // l 是左值, 输出左值
    25. return 0; }

    结果:

    传递右值:
    普通传参: 左值
    std::move 传参:  右值
    std::forward 传参:  右值
    static_cast 传参:  右值
    传递左值:
    普通传参: 左值
    std::move 传参:  右值
    std::forward 传参:  左值
    static_cast 传参:  左值

  • 相关阅读:
    二进制明文字符串加密:还原与反还原
    [云原生] [kubernetes] 基于K8S安装kubesphere
    5月9号软件资讯更新合集......
    软考高级系统架构设计师系列之:微服务
    编程语言参数传递方式和变量的两种基本类型
    Stable Diffusion WebUI如何做到真人和动漫互相转换,就在这里!
    c/c++单个文件或函数优化级别设置
    MYSQL之增删改查(中)
    图像处理软件Photoshop 2024 mac新增功能
    Elasticsearch:使用 Elasticsearch 进行语义搜索
  • 原文地址:https://blog.csdn.net/qq_62309585/article/details/126717622