Вопрос: Что такое лямбда-выражение в C ++ 11?


Что такое лямбда-выражение в C ++ 11? Когда я его буду использовать? Какой класс проблемы они решают, что было невозможно до их введения?

Было бы полезно несколько примеров, и примеры использования.


1169


источник


Ответы:


Проблема

C ++ включает полезные общие функции, такие как std::for_eachа также std::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);
}

Если вы используете только один раз и в этом конкретном месте, кажется, излишне писать целый класс, чтобы сделать что-то тривиальное и одно.

В 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 вводит lambdas, чтобы вы могли написать встроенный анонимный функтор для замены struct f, Для небольших простых примеров это может быть более чистым для чтения (оно хранит все в одном месте) и потенциально проще поддерживать, например, в простейшей форме:

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

Лямбда-функции - это просто синтаксический сахар для анонимных функторов.

Типы возврата

В простых случаях для вас выдается тип возврата лямбда, например:

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

однако, когда вы начинаете писать более сложные lambdas, вы быстро столкнетесь с случаями, когда тип возврата не может быть выведен компилятором, например:

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:

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;
            }
        });
}

«Захват» переменных

До сих пор мы не использовали ничего, кроме того, что было передано лямбда внутри него, но мы также можем использовать другие переменные в лямбда. Если вы хотите получить доступ к другим переменным, вы можете использовать предложение 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]захватывает переменные, такие как [&], но epsilon по значению
  • [=, &epsilon]захватывает переменные, такие как [=], но epsilon по ссылке

Сгенерированный operator()является constпо умолчанию, с импликацией, которую захватывают, будет constкогда вы обращаетесь к ним по умолчанию. Это приводит к тому, что каждый вызов с одним и тем же входом даст тот же результат, однако вы можете отметьте лямбда как mutableпросить, чтобы operator()это не const,


1179



Что такое лямбда-функция?

Концепция лямбда-функции C ++ исходит из лямбда-исчисления и функционального программирования. Лямбда - неназванная функция, которая полезна (в реальном программировании, а не в теории) для коротких фрагментов кода, которые невозможно повторно использовать и их не стоит называть.

В C ++ функция лямбда определяется как это

[]() { } // barebone lambda

или во всей красе

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

[]является списком захвата, ()список аргументов и {}тело функции.

Список захвата

Список захвата определяет, что извне лямбда должно быть доступно внутри тела функции и как. Это может быть:

  1. значение: [x]
  2. ссылка [& x]
  3. любая переменная, находящаяся в настоящее время в области по ссылке [&]
  4. то же, что и 3, но по значению [=]

Вы можете смешать любое из вышеперечисленного в списке, разделенном запятыми [x, &y],

Список аргументов

Список аргументов такой же, как и в любой другой функции C ++.

Тело функции

Код, который будет выполняться при вызове лямбда.

Возврат типа вычета

Если лямбда имеет только один оператор return, тип возврата может быть опущен и имеет неявный тип decltype(return_statement),

изменчивый

Если лямбда отмечена как изменяемая (например, []() mutable { }) разрешено изменять значения, которые были зафиксированы по значению.

Случаи использования

Библиотека, определенная стандартом ISO, в значительной степени отличается от lambdas и повышает удобство использования нескольких баров, так как теперь пользователям не нужно загромождать свой код небольшими функторами в некоторых доступных областях.

C ++ 14

В C ++ 14 лямбда были расширены различными предложениями.

Инициализированные снимки лямбда

Элемент списка захвата теперь можно инициализировать с помощью =, Это позволяет переименовывать переменные и захватывать, перемещаясь. Пример, взятый из стандарта:

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 может быть общим ( autoбудет эквивалентно Tздесь, если Tбыли аргументом шаблона типа где-то в окружении):

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

Улучшение вычитания возвращаемого типа

C ++ 14 позволяет выводить типы возврата для каждой функции и не ограничивает ее функциями вида return expression;, Это также распространяется на лямбда.


710



Лямбда-выражения обычно используются для инкапсуляции алгоритмов, чтобы они могли быть переданы другой функции. Однако, можно выполнить лямбду сразу после определения :

[&](){ ...your code... }(); // immediately executed lambda expression

функционально эквивалентно

{ ...your code... } // simple code block

Это делает лямбда-выражения мощный инструмент для рефакторинга сложных функций , Вы начинаете с упаковки раздела кода в лямбда-функции, как показано выше. Затем процесс явной параметризации можно выполнить постепенно с промежуточным тестированием после каждого шага. Как только вы полностью настроили кодовый блок (о чем свидетельствует удаление &), вы можете переместить код во внешнее местоположение и сделать его нормальной функцией.

Аналогично, вы можете использовать лямбда-выражения для инициализировать переменные на основе результата алгоритма ...

int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!

В виде способ разделения вашей логики программы , вы можете даже счесть полезным передать лямбда-выражение в качестве аргумента для другого выражения лямбда ...

[&]( std::function<void()> algorithm ) // wrapper section
   {
   ...your wrapper code...
   algorithm();
   ...your wrapper code...
   }
([&]() // algorithm section
   {
   ...your algorithm code...
   });

Лямбда-выражения также позволяют создавать именованные вложенные функции , что может быть удобным способом избежать дублирования логики. Использование именованных lambdas также имеет тенденцию быть немного легче на глазах (по сравнению с анонимными встроенными лямбдами) при передаче нетривиальной функции в качестве параметра другой функции. Примечание: не забудьте точку с запятой после закрытия фигурной скобки.

auto algorithm = [&]( double x, double m, double b ) -> double
   {
   return m*x+b;
   };

int a=algorithm(1,2,3), b=algorithm(4,5,6);

Если последующее профилирование показывает значительную служебную нагрузку инициализации для объекта функции, вы можете переписать это как обычную функцию.


145



Answers

Q: What is a lambda expression in C++11?

A: Under the hood it is object of autogenerated class with overloading operator() const. Such object is called closure and created by compiler. This 'closure' concept is near with the bind concept from C++11. But lambdas typically generate better code. And calls through closures allow full inlining.

Q: When would I use one?

A: To define "simple and small logic" and ask compiler perform generation from previous question. You give a compiler some expressions which you want to be inside operator(). All other stuff compiler will generate to you.

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

A: It is some kind of syntax sugar like operators overloading instead of functions for custom add, subrtact operations...But it save more lines of unneeded code to wrap 1-3 lines of real logic to some classes, and etc.! Some engineers think that if the number of lines is smaller then there is a less chance to make errors in it (I'm also think so)

Example of usage

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

Extras about lambdas, not covered by question. Ignore this section if you're not interest

1. Captured values. What you can to capture

1.1. You can reference to variable with static storage duration in lamdas. They all are captured.

1.2. You can use lamda for capture values "by value". In such case captured vars will be copied to function object (closure).

[captureVar1,captureVar2](int arg1){}

1.3. You can capture be reference. & -- in this context mean reference, not pointers.

   [&captureVar1,&captureVar2](int arg1){}

1.4. It is exist notation to capture all non-static vars by value, or by reference

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5. It is exist notation to capture all non-static vars by value, or by reference and specify smth. more. Examples: Capture all not-static vars by value, but by reference capture Param2

[=,&Param2](int arg1){} 

Capture all not-static vars by reference, but by value capture Param2

[&,Param2](int arg1){} 

2. Return type deduction

2.1. Lambda return type can be deduced if lamda is one expression. Or you can explicitly specify it.

[=](int arg1)->trailing_return_type{return trailing_return_type();}

If lambda has more then one expression, then return type must be specified via trailing return type. Also similar syntax can be applied to auto functions and member-functions

3. Captured values. What you can not to capture

3.1. You can capture only local vars, not member variable of object.

4. Сonversions

4.1. lambda is not a function pointer and it is not an anonymous function, but can be implicitly converted to function pointer.

p.s.

  1. More about lambda grammar information information can be found in Working draft for Programming Language C++ #337, 2012-01-16, 5.1.2. Lambda Expressions, p.88

  2. In C++14 the extra feature which has named as "init capture" have been added. It allow to perform arbitarily declaration of closure data members:

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
    

27



A lambda function is an anonymous function that you create in-line. It can capture variables as some have explained, (e.g. http://www.stroustrup.com/C++11FAQ.html#lambda) but there are some limitations. For example, if there's a callback interface like this,

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

you can write a function on the spot to use it like the one passed to apply below:

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

But you can't do this:

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

because of limitations in the C++11 standard. If you want to use captures, you have to rely on the library and

#include <functional> 

(or some other STL library like algorithm to get it indirectly) and then work with std::function instead of passing normal functions as parameters like this:

#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');
    });
}

10



One of the best explanation of lambda expression is given from author of C++ Bjarne Stroustrup in his book ***The C++ Programming Language*** chapter 11 (ISBN-13: 978-0321563842):

What is a lambda expression?

A lambda expression, sometimes also referred to as a lambda function or (strictly speaking incorrectly, but colloquially) as a lambda, is a simplified notation for defining and using an anonymous function object. Instead of defining a named class with an operator(), later making an object of that class, and finally invoking it, we can use a shorthand.

When would I use one?

This is particularly useful when we want to pass an operation as an argument to an algorithm. In the context of graphical user interfaces (and elsewhere), such operations are often referred to as callbacks.

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

Here i guess every action done with lambda expression can be solved without them, but with much more code and much bigger complexity. Lambda expression this is the way of optimization for your code and a way of making it more attractive. As sad by Stroustup :

effective ways of optimizing

Some examples

via lambda expression

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';
         });
}

or via function

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'; 
           }
};

or even

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}); 
}

if u need u can name lambda expression like below:

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);
 }

Or assume another simple sample

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);
             }
             );
}

will generate next

0

1

0

1

0

1

0

1

0

1

0 sortedx - 1;x - 3;x - 4;x - 5;x - 6;x - 7;x - 33;

[] - this is capture list or lambda introducer: if lambdas require no access to their local environment we can use it.

Quote from book:

The first character of a lambda expression is always [. A lambda introducer can take various forms:

[]: an empty capture list. This implies that no local names from the surrounding context can be used in the lambda body. For such lambda expressions, data is obtained from arguments or from nonlocal variables.

[&]: implicitly capture by reference. All local names can be used. All local variables are accessed by reference.

[=]: implicitly capture by value. All local names can be used. All names refer to copies of the local variables taken at the point of call of the lambda expression.

[capture-list]: explicit capture; the capture-list is the list of names of local variables to be captured (i.e., stored in the object) by reference or by value. Variables with names preceded by & are captured by reference. Other variables are captured by value. A capture list can also contain this and names followed by ... as elements.

[&, capture-list]: implicitly capture by reference all local variables with names not men- tioned in the list. The capture list can contain this. Listed names cannot be preceded by &. Variables named in the capture list are captured by value.

[=, capture-list]: implicitly capture by value all local variables with names not mentioned in the list. The capture list cannot contain this. The listed names must be preceded by &. Vari- ables named in the capture list are captured by reference.

Note that a local name preceded by & is always captured by reference and a local name not pre- ceded by & is always captured by value. Only capture by reference allows modification of variables in the calling environment.

Additional

Lambda expression format

enter image description here

Additional references:


4



One problem it solves: Code simpler than lambda for a call in constructor that uses an output parameter function for initializing a const member

You can initialize a const member of your class, with a call to a function that sets its value by giving back its output as an output parameter.


1



Well, one practical use I've found out is reducing boiler plate code. For example:

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);
  //... 
}

Without lambda, you may need to do something for different bsize cases. Of course you could create a function but what if you want to limit the usage within the scope of the soul user function? the nature of lambda fulfills this requirement and I use it for that case.


1