C++11 新特性学习 ——《现代C++教程》阅读笔记:第3章-Lambda表达式、函数对象类和右值引用

线上阅读链接:https://changkun.de/modern-cpp/zh-cn/00-preface/
最近正在阅读链接上这本《现代C++教程》,发帖作为学习笔记,如有侵权,告知即删。

第3章 语言运行期的强化

​ 这一章介绍了Lambda表达式、函数对象包装器右值引用三个内容,其中Lambda表达式、右值引用都是是C++11引入的重要特性。

Lambda表达式,适用于“需要一个函数,但是又不想费力命名一个函数”的情况,① 可以近似理解为在函数内定义的类函数,包括捕获列表、参数列表、返回类型定义以及内部函数体等等部分,在调用时直接传入需要的参数,捕获列表允许值传递、引用传递和通过表达式实现的右值传递等多种方式;而 ② 泛型 Lambda 则是可以进一步省略对参数列表参数类型、返回类型的定义,让编译器自主实现类型推导

函数对象包装器,① 引入std::function统一了空捕获列表的lambda表达式、函数类型等能够被调用的类型,为这些函数、函数指针的存储、复制和调用提供了类型安全的容器;② 使用std::bindstd::placeholder处理“不能够一次性获取某个函数全部参数”的情况,实现部分参数的绑定和占位,在参数齐全后完成调用。

右值引用,① 使用T&&声明延长临时变量的声明周期,提供std::move方法将左值无条件转换为右值,用右值转换和移动操作替代了传统C++中的创建、拷贝、销毁原对象的操作,大大减小额外开销;② 针对引入右值引用后的引用坍缩规则,使用std::forward方法保证在传递函数时保持原来的参数类型,与static_cast<T&&>等效。

3.1 Lambda表达式

基础Lambda

Lambda函数实际上就是提供一种类似于匿名函数的特性,用于“需要一个函数,但又不想费力命名一个函数的情况”,其基本语法是如下

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

其中的“捕获”改变较难理解,Lambda表达式内部函数体默认不能使用函数体外部的变量,因此捕获列表起到传递外部参数的作用,但是在后续调用这一表达式只需传入参数,而无需传入捕获内容。

(1)值捕获,与参数传值类似,前提是该变量可以拷贝,被捕获的变量在Lambda表达式创建时即拷贝,而非调用时

int value = 1;
// 定义lambda表达式(值捕获)
auto copy_value = [value] {
    return value;
};
value = 100;
// 使用lambda表达式
auto stored_value = copy_value();

(2)引用捕获,与引用传参类似,保存引用,值会发生变化

int value = 1;
// 定义lambda表达式(引用捕获)
auto copy_value = [&value] {
    return value;
};
value = 100;
// 使用lambda表达式
auto stored_value = copy_value();

(3)隐式捕获,相对于前两种捕获方式,lambda表达式也可以不在[]中写出变量名称,而是在**[]中说明捕获类型**、在函数体内直接使用对应变量名,让编译器自行推导捕获列表,具体的:[&] 为引用捕获,[=] 为值捕获。

(4)【表达式捕获】,然而,上面这些捕获方式捕获的都是左值,而不能捕获右值。C++14开始,允许捕获的成员用任意的表达式进行初始化,如下面的代码中important是一个独占指针,不能被“=”值捕获到,此时将其转移为右值,在表达式中初始化。

#include <iostream>
#include <memory>  // std::make_unique
#include <utility> // std::move

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);
    };
    // lambda表达式的使用,这里的3,4对应了x,y
    std::cout << add(3,4) << std::endl;
}

应用:使用lambda表达式进行排序操作,为一些复杂功能创造可能

std::vector<int> v = {3, 1, 5, 4, 2};
sort(v.begin(), v.end(), [](int a, int b){
    return b < a;
});

泛型Lambda

从C++14开始,Lambda函数的形参可以使用auto关键字产生泛型:

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

3.2 函数对象包装器

std::function 可调用类型

Lambda表达式的本质是一个与函数对象类型相似的类类型(称为闭包类型)的对象(称为闭包对象),当lambda表达式的捕获列表为空时,还能够传唤为函数指针进行传递,如下:

#include <iostream>

using foo = void(int); // 定义函数类型, using 的使用见上一节中的别名语法
void functional(foo f) { // 参数列表中定义的函数类型 foo 被视为退化后的函数指针类型 foo*
    f(1); // 通过函数指针调用函数
}

int main() {
    auto f = [](int value) {
        std::cout << value << std::endl;
    };
    functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值
    f(1); // lambda 表达式调用
    return 0;
}

可以看到,有两种不同的调用方式:① 作为函数类型传递进行调用;② 直接调用lambda表达式。C++引入了std::function类型统一了这些能够被调用的对象的类型,统一称之为可调用类型

std::function的实例可以对任何可以调用的目标实体进行存储、复制和调用,相对于函数指针的调用来说,是一种类型安全的包裹。实际上就是为函数提供了容器,使得将函数、函数指针作为对象进行处理更加方便。

#include <functional>
#include <iostream>

int foo(int para) {
    return para;
}

int main() {
    // std::function 包装了一个返回值为 int, 参数为 int 的函数
    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;
}

std::bind和std::placeholder

std::bind用来绑定函数调用的参数,适用于我们“不能够一次性获得调用某个函数的全部参数”的情况,通过该函数,将部分参数提前绑定到函数身上,而使用std::placeholder 对未获得的参数进行占位,待参数齐全后完成调用。如:

#include <functional>
int foo(int a, int b, int c) {
    ;
}
int main() {
    // 将参数1,2绑定到函数 foo 上,
    // 但使用 std::placeholders::_1 来对第一个参数进行占位
    auto bindFoo = std::bind(foo, std::placeholders::_1, 1,2);
    // 这时调用 bindFoo 时,只需要提供第一个参数即可
    bindFoo(1);
}

3.3 右值引用

右值引用的引入解决了大量历史遗留问题,消除了诸如std::vectorstd::string等类的额外开销,才使函数对象类std::function成为了可能。

C++11引入的右值引用用&&表示,并且可以进行修改。

int && a = 10;
a = 100;

概念理解:左值、右值、右值的纯右值、将亡值

  • 左值lvalueleft value,赋值符号左边的值,更准确来说是在表达式之后可以持久存在的对象
  • 右值rvalueright value,右边的值,表达式结束后就不再存在的临时对象。(传统C++中与纯右值是同一概念)
  • 纯右值prvaluepure rvalue,纯粹的字面值(如10,true)或求值结果相当于字面量/匿名临时对象(如1+2),非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、lambda表达式都属于纯右值。(注意,字符串字面量是一个坐值,类型为const char数组)
  • 将亡值xvalueexpiring value,C++11为引入右值引用而提出的概念,也就是即将被销毁、却能够被移动的值。它定义了这样一种行为:临时的值能够被识别、同时又能够被移动。可以看下面一段代码的分析:
std::vector<int> foo() {
    std::vector<int> temp = {1, 2, 3, 4};
    return temp;
}

std::vector<int> v = foo();

在上面的代码中

  • 传统C++中,函数foo的返回值temp在内部创建之后赋值给vv获得这个对象后将整个temp拷贝一份,然后将temp销毁,这个过程中经历了分别进行了一次创建和拷贝,如果temp非常大,就会造成大量的额外开销
  • C++11之后,编译器做了一些工作,将对此处的左值temp进行隐式右值转换,进而vfoo局部返回的值进行移动,也即后面将提到的移动语义。

这里还要区分常量非常量的概念,常量定义时用 const 修饰,由此形成了(非)常量左/右值四种类型,它们的可以引用情况和使用场景如下:

值类型→
引用类型↓
非常量左值常量左值非常量右值常量右值使用场景
非常量左值引用YNNN
常量左值引用YYYY常用于类中构造拷贝构造函数
非常量右值引用NNYN移动语义、完美转发
常量右值引用NNYY无实际用途

参考:http://c.biancheng.net/view/7829.html

【右值引用和左值引用】

右值引用 T && 的声明可以让临时值的生命周期得以延长,C++11提供了 std::move 这个方法将左值参数无条件转换为右值。

不允许将非常量绑定到非左值上;常量允许绑定到到非左值上。

移动语义

传统C++中如果要实现对资源的移动操作,需要调用者先复制、再析构,或者自己实现移动对象的接口,这类似于搬家的时候,先把所有东西复制一份放入新家,然后把旧家所有东西全部扔掉或销毁,是不合理的操作。这里传统C++的问题在于没有区分移动和拷贝的概念,造成了大量不必要的数据拷贝,浪费了时间和空间,右值引用的出现恰好解决了这个问题,通过① 显式地使用 std::move 实现(需要声明 #include <utility> 头文件)或者② 函数返回值实现。

std::move 所做的工作实际就是将左值转换为右值

看下面两段代码是使用移动语义的案例(分别通过 ① ② 方式):

#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::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: " << str << std::endl;

    // 将使用 push_back(const T&&), 不会出现拷贝行为
    // 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
    // 这步操作后, str 中的值会变为空
    v.push_back(std::move(str));
    // 将输出 "str: "
    std::cout << "str: " << str << std::endl;

    return 0;
}
#include <iostream>
class A {
public:
    int *pointer;
    A():pointer(new int(1)) {
        std::cout << "构造" << pointer << std::endl;
    }
    A(A& a):pointer(new int(*a.pointer)) {
        std::cout << "拷贝" << pointer << std::endl;
    } // 无意义的对象拷贝
    A(A&& a):pointer(a.pointer) {  // 如果没有这一函数,下面的obj构造将自动调用上面的“拷贝”方式的构造函数
        a.pointer = nullptr;
        std::cout << "移动" << pointer << std::endl;
    }
    ~A(){
        std::cout << "析构" << pointer << std::endl;
        delete pointer;
    }
};
// 防止编译器优化
A return_rvalue(bool test) {
    A a,b;
    if(test) return a; // 等价于 static_cast<A&&>(a);
    else return b;     // 等价于 static_cast<A&&>(b);
}
int main() {
    A obj = return_rvalue(false);
    std::cout << "obj:" << std::endl;
    std::cout << obj.pointer << std::endl;
    std::cout << *obj.pointer << std::endl;
    return 0;
}

运行结果及解释

构造0x2425c20  // a被创建出来
构造0x2425c40  // b被创建出来
移动0x2425c40  // return b时,b作为右值引用传递到obj的构造函数中,指针被移动给obj
析构0  // return_rvalue函数运行结束,对涉及的临时变量进行析构,移动完成后b本身变为了nullptr
析构0x2425c20  // a的指针地址
obj:
0x2425c40
1
析构0x2425c40  // 程序结束,析构obj

完美转发

在传统C++中,我们不能对一个引用类型继续进行引用,但是C++由于右值引用的出现放宽了这一要求,产生了所谓“引用坍缩规则”,允许对引用(左/右)进行引用,遵循如下规则:

函数形参类型(参数定义类型)实参参数类型(调用传入类型)推导后形参类型(函数内使用类型)
T&左引用T&
T&右引用T&
T&&左引用T&
T&&右引用T&&
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); // 始终调用 reference(int&)    // 推导后形参类型
}
int main() {
    std::cout << "传递右值:" << std::endl;
    pass(1); // 1是右值, 但输出是左值       // 实参参数类型是右引用,但是进入pass后实际调用了 void reference(int& v)

    std::cout << "传递左值:" << std::endl;
    int l = 1;
    pass(l); // l 是左值, 输出左值            // 实参参数类型

    return 0;
}

因为有了这一引用坍缩规则的存在,可能造成右引用在传递过程中变成了左引用,因此就需要有某种规则,帮助我们在传递参数时保持原来的参数类型(左引用保持左引用,右引用保持右引用),这就有了 std::forward ,与static_cast<T&&> 等效。使用方式如下,如此一来,不造成任何多余的拷贝,同时也实现了函数实参的完美转发

void reference(int& v) {
    std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
    std::cout << "右值引用" << std::endl;
}

reference(std::forward<T>(v)); // 保持v原本的参数类型(左/右引用)
// 等效于
reference(static_cast<T&&>(v));

【循环语句中 auto&& 的完美转发】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值