【现代C++】2.强化语言运行期的强化

1.Lambda表达式

lambda表达式实际提供了一个类似匿名函数的特性,匿名函数是在需要一个函数,但是又不想费力去命名一个函数的情况下使用的。

1.1 Lambda表达式基本语法

[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型
{
    // 函数体
}

捕获列表分为以下几种:

1.1.1 值捕获

与参数传值类似,值拷贝的前提是变量可以拷贝,不同之处在于,被捕获的变量在Lambda表达式被创建时拷贝,而非调用时才拷贝:

void lambda_value_capture()
{
    int value = 1;
    auto copy_value = [value]{
        return value;
    };
    value = 100;
    auto stored_value = copy_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // 这时, stored_value == 1, 而 value == 100.
    // 因为 copy_value 在创建时就保存了一份 value 的拷贝
}

1.1.2 引用捕获

引用捕获保存的是引用,值会发生变化。

void lambda_reference_capture()
{
    int value = 1;
    auto copy_value = [&value]{
        return value;
    };
    value = 100;
    auto stored_value = copy_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // 这时, stored_value == 100, value == 100.
    // 因为 copy_value 保存的是引用
}

1.1.3 隐式捕获

捕获提供了Lambda表达式对外部值进行使用的功能,捕获列表的最常用的四种形式可以是:

  • []空捕获列表
  • [name1,name2,...]捕获一系列变量
  • [&]引用捕获,让编译器自行推导引用列表
  • [=]值捕获,让编译器自行推导值捕获列表

1.1.4 表达式捕获

从C++14开始,允许捕获的成员用任意的表达式进行初始化,这就允许了右值的值捕获,被声明的捕获变量类型会根据表达式进行判断,判断方式与使用auto本质上是相同:

#include <iostream>
#include <memory>
#include <utility>

void lambda_expression_capture()
{
    auto important = std::make_unique<int>(1);
    auto add = [v1 = 1,v2 = std::move(important)](int x,int y) -> int{
        return x+y+v1+(*v2);
    };
    std::cout << add(3,4) << std::endl;
}

important 是一个独占指针,是不能够被 "=" 值捕获到,这时候我们可以将其转移为右值,在表达式中初始化。

1.2 泛型Lambda

从c++14开始增加泛型lambda,Lambda函数的形式参数可以使用auto关键字来产生意义上的泛型

auto add = [](auto x,auto y)
{
    return x+y;
};

add(1,2);
add(1.1,2.2);

2.函数对象包装器

2.1 std::function

**C++11 std::function是一种通用、多态的函数封装,它的实例可以对任何可以调用的目标进行存储、复制和调用操作,它也是对 C++ 中现有的可调用实体的一种类型安全的包裹(相对来说,函数指针的调用不是类型安全的), 换句话说,就是函数的容器。当我们有了函数的容器之后便能够更加方便的将函数、函数指针作为对象进行处理。 **

#include <functional>
#include <iostream>

int foo(int para)
{
    return para;
}

int main()
{
    std::function<int(int)> func = foo;

    int important = 10;
    std::function<int(int)> func2 = [&](int value) -> int{
        return 1+value+important;
    };

    std::cout << func(10) << std::endl;
    std::cout << func2(10) << std::endl;
}

2.2 std::bind和std::placeholder

std::bind是用来绑定函数调用的参数的,它解决的需求是我们有时候可能并不一定能够一次性获得调用某个函数的全部参数,通过这个函数,我们可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用。例如:

int foo(int a,int b,int c)
{
    ;
}

int main()
{
    // 将参数1,2绑定到函数foo上
    // 但使用Std::plancholders::_1来对对一个参数进行占位
    auto bindFoo = std::bind(foo,std::placeholders::_1,1,2);
    // 此时调用bindFoo时,只需要提供一个参数即可
    bindFoo(1);
}

3.右值引用

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

3.1左值,右值的纯右值,将亡值、右值

左值 ( lvalue,left value ):左值是表达式后依然存在的持久对象

右值(rvalue,right value) :右值是表到时结束后将不再存在的临时变量

C++11中引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值和将亡值

纯右值(prvalue,pure rvalue) :纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。

将亡值(xvalue,expiring value) :是 C++11 为了引入右值引用而提出的概念(因此在传统 C++ 中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。

3.2 右值引用和左值引用

要拿到一个将亡值,就需要用到右值引用:T &&,其中 T 是类型。 右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。

C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值, 有了它我们就能够方便的获得一个右值临时对象,例如:

#include <iostream>
#include <string>

void reference(std::string& str) {
    std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {
    std::cout << "右值" << std::endl;
}

int main()
{
    std::string lv1 = "string,"; // lv1 是一个左值
    // std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
    std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值
    std::cout << rv1 << std::endl; // string,

    const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
    // lv2 += "Test"; // 非法, 常量引用无法被修改
    std::cout << lv2 << std::endl; // string,string,

    std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
    rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
    std::cout << rv2 << std::endl; // string,string,string,Test

    reference(rv2); // 输出左值

    return 0;
}

3.3 移动语义

传统C++通过拷贝构造函数和赋值操作符为类对象,设计了拷贝/复制的概念,但为了实现对资源的移动的操作,调用者必须使用先复制、再析构的方式,否则就需要自己实现移动对象的接口。传统的C++没有区分移动和拷贝的概念,造成大量的数据拷贝,浪费时间和空间。

为了避免无意义的拷贝,可以使用std::move语义来加强性能:

#include <iostream>
#include <utility>
#include <vector>
#include <string>

int main()
{
    std::string str = "Hello world";
    std::vector<std::string> v;
  
    // 将使用push_back(const T&) ,即产生拷贝行为
    v.push_back(str);

    // 将输出"str::hello world"
    std::cout << "str:" << std::endl;

    // 将使用push_back(const T&&),不会拷贝行为
    v.push_back(std::move(str));
    // 将输出 "str: "
    std::cout << "str: " << str << std::endl;

    return 0;
}

3.4 完美转发

完美转发,是让我们在传递参数的时候,保持原来的参数类型,左引用保持左引用,右引用保持右引用。可以使用std::forward来进行参数的转发(传递):

#include <iostream>
#include <utility>
void reference(int& v) {
    std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
    std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v) {
    std::cout << "              普通传参: ";
    reference(v);
    std::cout << "       std::move 传参: ";
    reference(std::move(v));
    std::cout << "    std::forward 传参: ";
    reference(std::forward<T>(v));
    std::cout << "static_cast<T&&> 传参: ";
    reference(static_cast<T&&>(v));
}
int main() {
    std::cout << "传递右值:" << std::endl;
    pass(1);

    std::cout << "传递左值:" << std::endl;
    int v = 1;
    pass(v);

    return 0;
}

输出结果为:

传递右值:
              普通传参: 左值引用
       std::move 传参: 右值引用
    std::forward 传参: 右值引用
static_cast<T&&> 传参: 右值引用
传递左值:
              普通传参: 左值引用
       std::move 传参: 右值引用
    std::forward 传参: 左值引用
static_cast<T&&> 传参: 左值引用

无论传递参数为左值还是右值,普通传参都会将参数作为左值进行转发;由于类似的原因,std::move()总会接收一个左值,从而转发调用了reference(int&&)输出右值引用。

唯独std::forward既没有造成任何多余的拷贝,同时完美转发了函数的实参给了内部调用的其他函数。

std::forward和std::move一样,没有做任何事情,std::move单纯的将左值转化为右值,std::forward也只是单纯的将参数做了一个类型的转换,从现象上来看std::forward <T>(v)和static_cast<T&&>(v)是完全一样的

参考文章:

详情可见:https://changkun.de/modern-cpp/zh-cn/03-runtime/#%E6%80%BB%E7%BB%93

posted @ 2024-02-27 21:05  -zx-  阅读(3)  评论(0编辑  收藏  举报