C++11中的Lambda基础

1、Lambda表达式的基本语法
语法:[] () specifiers exception attr ->ret { /*do something;*/}
[] : 可选捕获列表引入Lambda;
() : 可选参数列表;
specifiers exception attr : 可选,mutable、异常说明符或者noexcept、attributes;
->ret : 可选,追踪返回类型;
{} : Lambda函数体。

2、一些Lambda表达式示例

2.1 最简单的Lambda

[]{};

这是最小的Lambda表达式,用[]引入Lambda表达式,{}函数体内为空,该示例中不需要参数列表()。

2.2 有两个参数

[](float f, int a) { return a*f; }
[](int a, int b) {return a<b; }

该示例可能是最普遍的Lambda表达式之一,参数通过()部分传入Lambda,不需要返回类型因为编译器会自动推导。

2.3 追踪返回类型

[](MyClass t) -> int { auto a = t.compute(); print(a); return a; };

该示例中显式设置了返回类型。自C++11开始,追踪返回类型也可用于常规函数声明。

2.4 附加说明符

[x](int a, int b) mutable { ++x; return a < b; };
[](float param) noexcept { return param*param; };
[x](int a, int b) mutable noexcept { ++x; return a < b; };

该示例展示在Lambda函数体之前可以使用说明符。使用mutable也可以是noexcept使得能修改捕获到的变量。并且必须以 mutable noexcept的顺序书写,否侧编译器会抛出异常。

2.5 可选的()参数列表

[x] { std::cout << x; }; // 不需要()
[x] mutable { ++x; }; // 不能编译!
[x]() mutable { ++x; }; // 可以编译,mutable之前要求有()
[] noexcept { }; // 不能编译!
[]() noexcept { }; // 可以编译

在C++17和C++20中像constexpr或consteval说明符也是用相同的模式。

3、Lambda表达式的类型
因为编译器会为每一个Lambda(the closure type)生成一个独特的名称,无法预先“拼写”出它,所以必须使用auto或者decltype来推导类型。

auto myLambda = [](int a) -> double { return 2.0 * a; };

如果有两个看起来一样的Lambda:

auto firstLam = [](int x) { return x * 2; };
auto secondLam = [](int x) { return x * 2; };

即使后面的代码相同,它们的类型也是不一样的。编译器被要求为每一个Lambda声明特有的未命名类型。
下面的代码可以证明这一属性:

#include 
#include 
int main()
{
    const auto oneLam = [](int x) noexcept { return x * 2; };
    const auto twoLam = [](int x) noexcept { return x * 2; };
    std::cout<< std::is_same<decltype(oneLam), decltype(twoLam)>::value << std::endl;
}

Lambda表达式的大小:

#include 
#include 
int main() 
{
    const auto myLambda = [](int a) noexcept -> double { return 2.0 * a; };
    std::cout << "sizeof(myLambda) is " << sizeof(myLambda) << '\n';
    return 0;
}

Windows下输出是:
sizeof(myLambda) is 1
因为myLambda是一个无状态的Lambda,是一个空类,没有任何数据成员,所以它的最小尺寸是1字节。

可以将一个Lambda表达式拷贝给另一个Lambda表达式:

#include
#include 
int main() 
{
    const auto firstLam = [](int x) noexcept { return x * 2; };
    const auto secondLam = firstLam;
    std::cout<< std::is_same<decltype(firstLam), decltype(secondLam)>::value << std::endl;
}

但它们的类型仍会相同。

4、重载
C++11无法“重载”接收不同参数的Lambda表达式:

// 不能编译
auto lam = [](double param) { /* do something*/ };
auto lam = [](int param) { /* do something*/ };

5、捕获
[ ]不仅引入了Lambda而且保存了捕获到的变量的列表。捕获Lambda域以外的变量可以在Lambda函数体内存取它们。

C++11捕获语法 描述
[&] 按引用捕获所有的自动存储持续变量
[=] 按值(创建拷贝)捕获所有的自动存储持续变量
[x, &y] 显式按值捕获x,按引用捕获y
[args…] 捕获模板参数包,都按值
[&args…] 捕获模板参数包,都按引用
[this] 捕获成员函数里面的this指针
int x = 2, y = 3;
const auto l1 = []() { return 1; }; // 无捕获
const auto l2 = [=]() { return x; }; // Lambda内使用的所有变量都被拷贝
const auto l3 = [&]() { return y; }; // Lambda内使用的所有变量都被引用
const auto l4 = [x]() { return x; }; // 只有x按值(拷贝)捕获
// const auto lx = [=x]() { return x; }; // 错误语法,不需显式使用=拷贝x
const auto l5 = [&y]() { return y; }; // 只有y按引用捕获
const auto l6 = [x, &y]() { return x * y; }; // 按值捕获x,按引用捕获y
const auto l7 = [=, &x]() { return x + y; }; // 除了按引用捕获x,其他所有都按值捕获
const auto l8 = [&, y]() { return x - y; }; // 除了按值捕获y,其他所有都按引用捕获

为了理解捕获变量时发生了什么,可以考虑下面的示例:
例1:

std::string str {"Hello World"};
auto foo = [str]() { std::cout << str << '\n'; };
foo();

str被按值捕获(被拷贝)。编译器会生成下面的本地函数对象:

struct _unnamedLambda {
inline void operator()() const {
std::cout << str << '\n';
}
std::string str;
};

将变量传入捕获从句时,直接用它初始化数据成员str。

例2:

int x = 1, y = 1;
std::cout << x << " " << y << '\n';
const auto foo = [&x, &y]() noexcept { ++x; ++y; };
foo();
std::cout << x << " " << y << '\n';

编译器可能会生成下面的本地函数对象:

struct _unnamedLambda {
void operator()() const noexcept {
++x; ++y;
}
int& x;
int& y;
};

因为按引用捕获了x和y,所以数据成员的类型也是引用。

6、关键字mutable
默认情况下,运算符()被标记为const,所以不能在Lambda函数体内部修改捕获到的变量。

#include
int main()
{
  int x = 1;
  auto foo = [x](){ };
}

上面的代码在CPPInsights中的运行结果为:

#include
int main()
{
  int x = 1;
    
  class __lambda_5_14
  {
    public: 
    inline void operator()() const
    {
    }
    
    private: 
    int x;
    public: 
    // inline /*constexpr */ __lambda_5_14(__lambda_5_14 &&) noexcept = default;
    __lambda_5_14(int & _x)
    : x{_x}
    {}
    
  };
  
  __lambda_5_14 foo = __lambda_5_14(__lambda_5_14{x});
  return 0;
}

因此下面的代码编译器会提示错误:

int x=0;
auto lam = [x]() { ++x; };

C++11中的Lambda基础_第1张图片
如果想要改变这种行为,需要在参数列表之后加上mutable关键字。这个语法有效地移除调用运算符声明中的const。

#include
int main()
{
  int x = 1;
  auto foo = [x]() mutable { };
}
#include
int main()
{
  int x = 1;
    
  class __lambda_5_14
  {
    public: 
    inline void operator()()
    {
    }
    
    private: 
    int x;
    public: 
    // inline /*constexpr */ __lambda_5_14(__lambda_5_14 &&) noexcept = default;
    __lambda_5_14(int & _x)
    : x{_x}
    {}
    
  };
  
  __lambda_5_14 foo = __lambda_5_14(__lambda_5_14{x});
  return 0;
}

下面的代码解释了这一行为:

int x=0;
    std::cout<< x<<std::endl;
    auto lam = [x]() mutable {++x; std::cout<< x<<std::endl; };
    lam();//注意!像使用函数那样使用Lamda表达式,声明Lambda表达式时不需要加(),但使用时必须加上
    std::cout<< x<<std::endl;

输出结果:
C++11中的Lambda基础_第2张图片
实际上Lambda只修改了变量的拷贝,并没有真正地修改变量。要实现Lambda体内修改外部变量,可以考虑使用按引用捕获。

int x=0;
    std::cout<< x<<std::endl;
    auto lam = [&x](){++x; std::cout<< x<<std::endl; };
    lam();//注意!像使用函数那样使用Lamda表达式,声明Lambda表达式时不需要加(),但使用时必须加上
    std::cout<< x<<std::endl;

输出结果:
C++11中的Lambda基础_第3张图片
也可以加入关键字noexcept:

int x=0;
    std::cout<< x<<std::endl;
    auto lam = [&x]() noexcept {++x; std::cout<< x<<std::endl; };
    lam();//注意!像使用函数那样使用Lamda表达式,声明Lambda表达式时不需要加(),但使用时必须加上
    std::cout<< x<<std::endl;

输出结果同上一示例。

需要注意的一件重要的事情是,当使用mutable时不能将Lambda表达式标记为const:

int x = 10;
const auto lam = [x]() mutable { ++x; }
lam(); // 不能编译,因为不能对const对象调用非const成员函数

7、捕获全局变量和局部静态变量
考察下面的例子:

#include 
int global = 10;
int main()
{
    std::cout << global << '\n';
    auto foo = [=]() mutable noexcept { ++global; };
    foo();
    std::cout << global << '\n';
    const auto increaseGlobal = []() noexcept { ++global; };
    increaseGlobal();
    std::cout << global << '\n';
    const auto moreIncreaseGlobal = []() noexcept { ++global; };
    moreIncreaseGlobal();
    std::cout << global << '\n';
}

输出结果:
C++11中的Lambda基础_第4张图片

#include 
void bar()
{
    static int static_int = 10;
    std::cout << static_int << '\n';
    auto foo = [=]() mutable noexcept{ ++static_int; };
    foo();
    std::cout << static_int << '\n';
    const auto increase = []() noexcept { ++static_int; };
    increase();
    std::cout << static_int << '\n';
    const auto moreIncrease = []() noexcept { ++static_int; };
    moreIncrease();
    std::cout << static_int << '\n';
}
int main()
{
    bar();
}

输出结果:
C++11中的Lambda基础_第5张图片

因为只有自动存储持续变量才能被捕获,所以显式捕获全局变量时会出现警告:

const auto moreIncreaseGlobal = [global]() noexcept { ++global; };

Clang编译器会生成错误:

error: ‘global’ cannot be captured because it does not have automatic storage duration

使用[static_int]捕获局部静态变量时,Clang也会显示同样的错误。
8、捕获类成员和this指针
捕获类成员时要在捕获列表里面加上this。

#include 
class Baz
{
private:
    std::string s="";
    int i=0;
    char c='\0';
public:
    Baz()
    {
        s="Basic Lambdas in C++11";
        i=100;
        c='C';
    }
    void foo()
    {
        const auto lam = [this]()
        {
            std::cout << s <<'\n' << i << '\n' << c <<'\n';
        };
        lam();
    }
};
int main()
{
    Baz b;
    b.foo();
}

输出结果:
C++11中的Lambda基础_第6张图片
参考文献:
深蓝学院C++基础与深度解析
C++ Lambda Story by Bartłomiej Filipek
https://cppinsights.io/

你可能感兴趣的:(备忘录模式,开发语言)