C++11新特性 lambda表达式

lambda表达式是现代编程语言的一个基础特性,比如LISP、Python、C#等具备该特性。而C++在C++11标准才正式支持lambda表达式。

一、定义

C++11中,lambda表达式的语法非常简单:

[ captures ] ( params ) specifiers exception -> ret { body }

举个例子

int main() {
    int x = 5;
    auto foo = [x](int y)->int { return x * y; };
    std::cout << foo(8) << std::endl;
}
  • captures 捕获列表,上例中捕获了变量 x

  • params 可选参数列表,上例中添加了参数 y

  • specifiers 可选限定符,C++11中可以用mutable,它允许我们在lambda表达式函数体内改变按值捕获的变量,或者调用非const的成员函数

  • exception 可选异常说明符,可以使用noexcept来指明lambda是否会抛出异常

  • ret 可选返回值类型

  • body lambda表达式的函数体

由于大量的内容都是可选的,所以最简单的lambda表达式是 [](),虽然很奇怪,但是它合法。

二、捕获列表

2.1、作用域

捕获列表中的变量存在于两个作用域——lambda表达式定义的函数作用域以及lambda表达式函数体的作用域。前者是为了捕获变量,后者是为了使用变量。

捕获的变量必须是一个自动存储类型(非静态局部变量)。

如果我们想要在lambda中使用静态变量,或者一个全局变量怎么办,直接用就好了,本身就在这些变量的作用域内

int x = 1;
int main() {
    int y = 2;
    static int z = 3;
    auto foo = [y] { return x + y + z; };
    std::cout << foo() << std::endl;
}

2.2、捕获引用

前面我们都是在捕获值,现在我们来捕获引用

void bar1() {
    int x = 5, y = 8;
    auto foo = [x, y] {
        x += 1;             // 编译失败,无法改变捕获变量的值
        y += 2;             // 编译失败,无法改变捕获变量的值
        return x * y;
    };
    std::cout << foo() << std::endl;
}


void bar2() {
    int x = 5, y = 8;
    auto foo = [&x, &y] {
        x += 1;
        y += 2;
        return x * y;
    };
    std::cout << foo() << std::endl;
}

为什么bar1会出现编译错误,因为lambda表达式的一个特性:捕获的变量默认为常量。在捕获引用的情况下,捕获变量实际上是一个引用,我们在函数体内改变的并不是引用本身,而是引用的值,所以并没有被编译器拒绝。

mutable说明符可以移除lambda表达式的常量性:

void bar3() {
    int x = 5, y = 8;
    auto foo = [x, y] () mutable {
        x += 1;
        y += 2;
        return x * y;
    };
    std::cout << foo() << std::endl;
}

上述lambda增加说明符mutable,还多了一对(),这是因为语法规定lambda表达式如果存在说明符,那么形参列表不能省略

编译运行bar2和bar3两个函数会输出相同的结果,但这并不代表两个函数是等价的,捕获值和捕获引用还是存在着本质区别。当lambda表达式捕获值时,表达式内实际获得的是捕获变量的复制,我们可以任意地修改内部捕获变量,但不会影响外部变量。

对于捕获值的lambda表达式还有一点需要注意,捕获值的变量在lambda表达式定义的时候已经固定下来了,无论函数在lambda表达式定义后如何修改外部变量的值,lambda表达式捕获的值都不会变化

int main() {
    int x = 5, y = 8;
    auto foo = [x, &y]() mutable {
        x += 1;
        y += 2;
        std::cout << "lambda x = " << x << ", y = " << y << std::endl;
        return x * y;
    };
    x = 9;
    y = 20;
    foo();
}

运行结果为:

lambda x = 6, y = 22

2.3、广义捕获

广义捕获有两种方式,第一种是C++11中支持的简单捕获:

1.[this] —— 捕获this指针,捕获this指针可以让我们使用this类型的成员变量和函数。

2.[=] —— 捕获lambda表达式定义作用域的全部变量的值,包括this。

3.[&] —— 捕获lambda表达式定义作用域的全部变量的引用,包括this。

第二种是C++14中支持的初始化捕获:他解决了简单捕获中只能捕获lambda表达式上下文变量,无法捕获表达式结果以及无法自定义捕获变量名的问题。

int main() {
    int x = 5;
    auto foo = [x = x + 1]{ return x; };
}

这个lambda表达式在C++11是无法编译通过的,但是在C++14可以。这个赋值表达式通过等号跨越了两个作用域,等号左边的变量存在于lambda表达式的作用域,等号右边的变量存在于main函数的作用域。还可以这样写

int main() {
    int x = 5;
    auto foo = [y = x + 1]{ return y; };
}

如果此时在lambda表达式函数体里使用变量x,则会出现编译错误。

初始化捕获在一些场景下可以起到减少运行开销的作用:比如下面这个例子:

int main() {
    std::string x = "hello c++ ";
    auto foo = [x = std::move(x)]{ return x + "world"; };
}

使用std::move对捕获列表变量x进行初始化,这样避免了简单捕获的复制对象操作,代码运行效率得到了提升。

还有就是是在异步调用时复制this对象,防止lambda表达式被调用时因原始this对象被析构造成未定义的行为:

class Work {
private:
	int value;

public:
    Work() : value(42) {}
    std::future<int> spawn() {
        // 返回32766,因为this指针被析构,无定义行为
    	return std::async([=]() -> int { return value; });
    }
    
    std::future<int> spawn() {
        // 返回42,this复制到tmp对象中,然后在函数体内返回tmp对象的value
    	return std::async([=, tmp = *this]() -> int { return tmp.value; });
    }
};

std::future<int> foo() {
    Work tmp;
    return tmp.spawn();
}

int main() {
    std::future<int> f = foo();
    f.wait();    
    std::cout << "f.get() = " << f.get() << std::endl;
}

三、无状态lambda表达式

C++标准对于无状态的lambda表达式有着特殊的照顾,即它可以隐式转换为函数指针

void f(void(*)()) {}
void g() { f([] {}); } // 编译成功

在上面的代码中,lambda表达式[] {}隐式转换为void(*)()类型的函数指针。同样,看下面的代码:

void f(void(&)()) {}
void g() { f(*[] {}); }

这段代码也可以顺利地通过编译。我们经常会在STL的代码中遇到lambda表达式的这种应用

四、在STL中使用lambda表达式

在STL中常常会有这样一些函数,它们的形参需要传入一个函数指针或函数对象从而完成整个算法,例如std::sortstd::find_if等,在C++11标准之前,通常这个函数需要在外部定义。而lambda表达式可以直接在STL算法函数的参数列表内实现辅助函数

int main() {
    std::vector<int> x = {1, 2, 3, 4, 5};
    std::cout << *std::find_if(x.cbegin(), x.cend(), [](int i) { return (i % 3) == 0; }) << std::endl;
}

五、泛型lambda表达式

C++14标准让lambda表达式具备了模版函数的能力,我们称它为泛型lambda表达式。虽然具备模版函数的能力,但是它的定义方式却用不到template关键字。实际上泛型lambda表达式语法要简单很多,我们只需要使用auto占位符即可:

int main() {
    auto foo = [](auto a) { return a; };
    int three = foo(3);
    char const* hello = foo("hello");
}

六、捕获[=,*this]

在广义捕获中,我们在捕获列表内复制了一份this指向的对象到tmp,然后使用tmp的value。这样并不优美,如果在lambda表达式中用到了大量this指向的对象,那么必须将所有对象全部修改。为了更方便地复制和使用this对象,C++17增加了捕获列表的语法来简化这个操作,具体来说就是在捕获列表中直接添加[*this],然后在lambda表达式函数体内直接使用this指向对象的成员

class Work{
private:
    int value;

public:
    Work() : value(42) {}
    std::future<int> spawn() {
        return std::async([=, *this]() -> int { return value; });
    }
};

简化了操作,可以看做是语法糖

七、捕获[=,this]

[=]可以捕获this指针,相似的,[=,\*this]会捕获this对象的副本。但是在代码中大量出现[=]和[=,\*this]的时候我们可能很容易忘记前者与后者的区别。为了解决这个问题,在C++20标准中引入了[=, this]捕获this指针的语法,它实际上表达的意思和[=]相同,目的是为了方便区分。

[=, this]{}; // C++17 编译报错或者报警告, C++20成功编译

同时用两种语法捕获this指针是不允许的,比如:

[this, *this]{};
  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LyaJpunov

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值