运行时增强

运行时增强

fetch150zy

语言运行期的强化

Lambda表达式

提供类似匿名函数的特性

基础

concept

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

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

    :unamused: example

    1
    2
    3
    4
    5
    int value = 1;
    auto copy_value = [value]() -> int {
    // [value] {} 省略写法
    return value;
    }
  2. 引用捕获

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

    example

    1
    2
    3
    4
    5
    6
    int value = 1;
    auto copy_value = [&value]() -> int {
    return value;
    }
    value = 100;
    copy_value(); // 100
  3. 隐式捕获

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

    上面提到的值捕获、引用捕获都是在外层作用域声明的变量,因此这些捕获方式捕获的都是左值,而不是捕获右值

    C++14允许捕获的成员用任意的表达式进行初始化(也就允许了右值捕获)

    example

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

    important是一个是一个独占指针,是不能够被=捕获到,可以将其转移为右值

泛型lambda

auto关键字是不能用在参数列表中的(C++20起可通过概念和约束实现),因为这种写法会与模板的功能有冲突

但Lambda表达式并非普通函数,在没有明确指名参数表类型的情况下,Lambda表达式并不能进行模板化

C++14起,Lambda函数的形式参数可以通过auto来产生意义上的泛型

example

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

函数对象包装器

std::function

Lambda表达式的本质是一个和函数对象类似的类类型(闭包类型)的对象(闭包对象)

tips: 当Lambda表达式的捕获列表为空时,闭包对象还能够转换为函数指针值进行传递

C++11起,将能够被调用的对象的类型,统一称为可调用类型(std::function)

是一种通用、多态的函数封装,其实例可以对任何可以调用的目标实体进行存储、复制和调用类型(也是对C++现有的可调用实体的一种类型安全的包裹/函数指针的调用不是类型安全的)

example

1
2
3
4
std::function<int(int)> func = foo;	// 返回值和参数都为int类型
std::function<int(int)> func2 = [&](int value) -> int {
return xxx;
}

std::bind & std::placeholder

std::bind:用来绑定函数调用参数的(有时并不能够一次性获取调用某个函数的全部参数)

通过这个函数,可以将函数调用部分参数提前绑定到函数身上成为一个新的对下稿,然后在参数齐全后完成调用

example

1
2
3
4
int foo(int a, int b, int c) {}
// 将参数1,2绑定到foo上,并使用std::placeholders::_1来对第一个参数占位
auto bindFoo = std::bind(foo, std::placeholders::_1, 1, 2);
// 这时调用bindFoo时只需要提供第一个参数即可

右值引用

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

  1. 左值:表达式结束后依然存在的持久对象

  2. 右值:表达式结束后不再存在的临时对象

  3. 纯右值:纯粹的字面量,求值结果相当于字面量或匿名临时对象,Lambda表达式

tips: 字面量除了字符串字面量之外均为纯右值,而字符串字面量是一个左值,类型为const char数组

  1. 将亡值:即将被销毁,却能够移动的值

    1
    2
    3
    4
    5
    std::vector<int> foo() {
    std::vector<int> temp = {1, 2, 3, 4};
    return temp;
    }
    std::vector<int> v = foo();

左值引用和右值引用

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

C++11提供了std::move这个方法将左值参数无条件的转移为右值

example

1
2
3
4
5
std::string lv1 = "xxx"; // lv1为左值
std::string&& rv1 = std::move(lv1); // std::move将左值转右值

const std::string& lv2 = lv1 + lv1; // 常量左值能够延长临时变量的声明周期
std::string&& rv2 = lv1 + lv2; // 右值引用能延长临时对象声明周期

一个历史遗留问题:

1
2
int& a = std::move(1); // invalid, 存在逻辑错误
const int& b = std::move(1); // valid, Fortran需要

移动语义

传统C++没有区分移动和拷贝的概念,造成大量的数据拷贝,浪费空间和时间

完美转发

一个声明的右值引用其实是一个左值

issue

1
2
3
4
5
6
7
8
9
10
11
void reference(int &v) {}
void reference(int &&v) {}

template<typename T> void pass(T&& t) {
// 此处t其实是一个左值
reference(t);
}

pass(1); // call refernece(int &v)
int v = 1;
pass(v); // call reference(int &v)

v是一个左值,为什么可以传递给pass(T&&)呢?

  • 引用坍缩规则

    函数形参类型 实参参数类型 推导后函数形参类型
    T& 左引用 T&
    T& 右引用 T&
    T&& 左引用 T&
    T&& 右引用 T&&

无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型

为了在传递参数的时候,保持原来的参数类型(左引用保持左引用,右引用保持右引用)使用std::forward来进行参数传递

example

1
2
3
4
5
6
7
8
9
void reference(int &v) {}
void reference(int &&v) {}

template<typename T> void pass(T&& t) {
reference(t); // always call reference(int&)
reference(std::move(t)); // always call reference(int&&)
reference(std::forward<T>(t)); // 完美转发,取决于pass接受的参数
reference(static_cast<T&&>(v)); // 现象与完美转发相同
}