sort - 什么是C++ 11中的lambda表达式?




lambda c++11 (6)

什么是lambda函数?

lambda函数的C ++概念来源于lambda演算和函数式编程。 lambda是一个未命名的函数,对于不可重用且不值得命名的代码片段很有用(在实际编程中,而非理论上)。

在C ++中,lambda函数是这样定义的

[]() { } // barebone lambda

或者所有的荣耀

[]() mutable -> T { } // T is the return type, still lacking throw()

[]是捕获列表, ()参数列表和{}函数体。

捕获列表

捕获列表定义了lambda外部应该在函数体内可用的内容以及如何实现。 它可以是:

  1. 值:[x]
  2. 参考文献[&x]
  3. 目前在参考范围内的任何变量[&]
  4. 与3相同,但按值[=]

你可以用逗号分隔的列表[x, &y]混合上述任何一个。

参数列表

参数列表与其他C ++函数中的参数列表相同。

功能体

实际调用lambda时将执行的代码。

返回类型扣除

如果lambda只有一个return语句,则返回类型可以省略,并且具有隐式类型的decltype(return_statement)

易变的

如果一个lambda被标记为mutable(例如[]() mutable { } ),那么它允许突变已被值捕获的值。

用例

由ISO标准定义的库很大程度上受益于lambda表达式,并提高了可用性的几个方面,因为现在用户不必在一些可访问范围内使用小函数来混淆他们的代码。

C ++ 14

在C ++中,14个lambda通过各种提议得到了扩展。

初始化的Lambda捕获

捕获列表的一个元素现在可以用=来初始化。 这允许重命名变量并通过移动来捕获。 从标准中取得一个例子:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

和一个从维基百科显示如何捕捉std::move

auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

通用Lambdas

Lambdas现在可以是泛型的(如果T是周围范围内的某个类型模板参数, auto在这里等于T ):

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

改进的退货类型扣除

C ++ 14允许为每个函数推导返回类型,并且不会将其限制为形式return expression;函数return expression; 。 这也延伸到lambda。

什么是C ++ 11中的lambda表达式? 我什么时候可以用一个? 在他们推出之前,他们解决了哪一类问题是不可能的?

一些例子和用例会很有用。


问题

C ++包含有用的通用函数,如std::for_eachstd::transform ,它们可以非常方便。 不幸的是,它们的使用也很麻烦,特别是如果您想要应用的函数对于特定函数是独一无二的。

#include <algorithm>
#include <vector>

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector<int>& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

如果你只在某个地方使用f,那么写一整堂课只是为了做一件简单的事,而不是一件事。

在C ++ 03中,你可能会试图写下如下内容,以保持函子本地:

void func2(std::vector<int>& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

然而这是不允许的, f不能传递给C ++ 03中的模板函数。

新的解决方案

C ++ 11引入lambda允许你编写一个内联的匿名函数来替换struct f 。 对于小的简单例子,它可以更清晰地阅读(它将所有内容保存在一个地方),并且可能更容易维护,例如以最简单的形式:

void func3(std::vector<int>& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

Lambda函数只是匿名函数的语法糖。

返回类型

在简单情况下,lambda的返回类型是为您推导的,例如:

void func4(std::vector<double>& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

但是当你开始编写更复杂的lambda时,你会很快遇到编译器无法推导出返回类型的情况,例如:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

为了解决这个问题,你可以使用-> T来明确指定一个lambda函数的返回类型:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

“捕捉”变量

到目前为止,我们还没有使用除传递给它内部lambda以外的任何内容,但我们也可以在lambda内使用其他变量。 如果你想访问其他变量,你可以使用capture子句(表达式的[] ),这个例子中迄今还没有被使用,例如:

void func5(std::vector<double>& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

您可以通过引用和值来捕获,您可以分别使用&=指定值:

  • [&epsilon]通过引用捕获[&epsilon]
  • [&]通过引用捕获lambda中使用的所有变量
  • [=]通过值捕获lambda中使用的所有变量
  • [&, epsilon]用[& [&, epsilon]捕获变量,但是按值赋值为epsilon
  • [=, &epsilon] ε通过引用捕获变量,如[=],但是epsilon

生成的operator()默认为const ,默认情况下,当您访问它们时,捕获的含义将为const 。 这样做的结果是每个具有相同输入的调用都会产生相同的结果,但是您可以将lambda标记为mutable以请求生成的operator()不是const


lambda函数是您创建的匿名函数。 它可以捕获一些已经解释过的变量(例如http://www.stroustrup.com/C++11FAQ.html#lambda ),但有一些限制。 例如,如果有这样的回调接口,

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

你可以在现场编写一个函数来使用它,就像下面通过的函数一样:

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

但是你不能这样做:

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

因为C ++ 11标准的局限性。 如果你想使用捕获,你必须依靠图书馆和

#include <functional> 

(或其他一些类似于算法的STL库来间接获取它),然后使用std :: function而不是像下面这样的参数传递普通函数:

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}

lambda expression的最佳解释之一是由C ++ Bjarne Stroustrup的作者在他的书***The C++ Programming Language***第11章( ISBN-13:978-0321563842 )中给出的:

What is a lambda expression?

一个lambda表达式 ,有时也被称为lambda函数,或者(严格来说不正确,但俗称)作为lambda表达式 ,是定义和使用匿名函数对象的简化符号。 我们没有用operator()定义一个命名类,而是后来创建该类的一个对象,最后调用它,我们可以使用简写。

When would I use one?

当我们想将操作作为参数传递给算法时,这是特别有用的。 在图形用户界面(以及其他地方)的情况下,这样的操作通常被称为回调

What class of problem do they solve that wasn't possible prior to their introduction?

在这里,我想每个用lambda表达式完成的动作都可以在没有它们的情况下解决,但是代码更多,复杂度也更高。 Lambda表达式这是您的代码优化方式,也是让它更具吸引力的一种方式。 Stroustup感到难过:

有效的优化方法

Some examples

通过lambda表达式

void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
    for_each(begin(v),end(v),
        [&os,m](int x) { 
           if (x%m==0) os << x << '\n';
         });
}

或通过功能

class Modulo_print {
         ostream& os; // members to hold the capture list int m;
     public:
         Modulo_print(ostream& s, int mm) :os(s), m(mm) {} 
         void operator()(int x) const
           { 
             if (x%m==0) os << x << '\n'; 
           }
};

甚至

void print_modulo(const vector<int>& v, ostream& os, int m) 
     // output v[i] to os if v[i]%m==0
{
    class Modulo_print {
        ostream& os; // members to hold the capture list
        int m; 
        public:
           Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
           void operator()(int x) const
           { 
               if (x%m==0) os << x << '\n';
           }
     };
     for_each(begin(v),end(v),Modulo_print{os,m}); 
}

如果你需要你可以像下面这样命名lambda expression

void print_modulo(const vector<int>& v, ostream& os, int m)
    // output v[i] to os if v[i]%m==0
{
      auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
      for_each(begin(v),end(v),Modulo_print);
 }

或者假设另一个简单样本

void TestFunctions::simpleLambda() {
    bool sensitive = true;
    std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});

    sort(v.begin(),v.end(),
         [sensitive](int x, int y) {
             printf("\n%i\n",  x < y);
             return sensitive ? x < y : abs(x) < abs(y);
         });


    printf("sorted");
    for_each(v.begin(), v.end(),
             [](int x) {
                 printf("x - %i;", x);
             }
             );
}

将生成下一个

0

1

0

1

0

1

0

1

0

1

0排序x - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;

[] - 这是捕获列表或lambda introducer :如果lambdas不需要访问他们的本地环境,我们可以使用它。

从书中引用:

lambda表达式的第一个字符总是[ 。 lambda介绍人可以采取各种形式:

[] :空的捕获列表。 这意味着在lambda体中不能使用周围环境的本地名称。 对于这样的lambda表达式,数据是从参数或非本地变量中获得的。

[&] :通过引用隐式捕获。 所有本地名称都可以使用。 所有本地变量都通过引用来访问。

[=] :按值隐式捕获。 所有本地名称都可以使用。 所有名称都是指在lambda表达式的调用点处获取的局部变量的副本。

[捕获列表]:明确捕获; 捕获列表是要通过引用或按值捕获(即,存储在对象中)的局部变量的名称的列表。 名称前带有&的变量被引用捕获。 其他变量是通过值捕获的。 捕获列表还可以包含这个和名称,后面跟着......作为元素。

[&,capture-list] :通过引用隐式地捕获列表中未提及名称的所有局部变量。 捕获列表可以包含这个。 列出的名字不能以&开头。 捕获列表中指定的变量是通过值捕获的。

[=,capture-list] :通过值隐式地捕获列表中未提及名称的所有局部变量。 捕获列表不能包含这个。 列出的名称必须以“&”开头。 捕获列表中命名的变量通过引用被捕获。

请注意,以&开头的本地名称始终由引用捕获,并且本地名称不会由&始终由值捕获。 只有通过引用捕获才能修改调用环境中的变量。

Additional

Lambda expression格式

其他参考:



那么,我发现的一个实际用途是减少锅炉板代码。 例如:

void process_z_vec(vector<int>& vec)
{
  auto print_2d = [](const vector<int>& board, int bsize)
  {
    for(int i = 0; i<bsize; i++)
    {
      for(int j=0; j<bsize; j++)
      {
        cout << board[bsize*i+j] << " ";
      }
      cout << "\n";
    }
  };
  // Do sth with the vec.
  print_2d(vec,x_size);
  // Do sth else with the vec.
  print_2d(vec,y_size);
  //... 
}

没有lambda,你可能需要为不同的bsize情况做些事情。 当然你可以创建一个函数,但是如果你想限制灵魂用户函数范围内的用法呢? 拉姆达的性质满足了这个要求,我在这种情况下使用它。





c++-faq