C++ лямбда-выражения: предложение Capture vs Argument List; в чем принципиальная разница?
Я изучаю лямбда-выражения в C++, хотя я не новичок в C/C++. Мне трудно увидеть относительные преимущества использования Capture-Clause по сравнению со старомодной передачей параметров в Argument-List для рисования переменных в теле Lambda для манипуляции. Я знаком с их синтаксическими различиями, а также с тем, что в каждом из них разрешено и не разрешено, но я просто не понимаю, насколько один из них эффективнее другого?
Если у вас есть инсайдерские знания или лучшая картина того, что происходит с Lambdas, пожалуйста, дайте мне знать.
Большое спасибо, Реза.
3 ответа
Учтите, что лямбды - это просто синтаксический сахар для функторов. Например
int x = 1;
auto f = [x](int y){ return x+y; };
это более или менее эквивалентно
struct add_x {
int x;
add_x(int x) : x(x) {}
int operator()(int y) { return y; }
}
int x = 1;
add_x f{x};
И разница становится очевидной, когда вы передаете лямбда, например,
template <typename F>
void foo(F f) {
for (int i=0;i<10;++i) std::cout << f(i) << '\n';
}
Подобные функции являются одной из основных причин использования лямбды, и именно эта функция (в данном случае только неявно) определяет ожидаемую сигнатуру. Вы можете позвонить foo
как
foo(f);
Но если бы ваш функтор / лямбда взял бы также x
в качестве параметра, то вы не сможете передать его foo
,
TL;DR: перехваченные переменные составляют состояние лямбды, тогда как параметры аналогичны параметрам обычной функции.
Лямбда-выражение создает функциональный объект с некоторым необязательным дополнительным состоянием. Сигнатура вызова определяется параметрами лямбды, а дополнительное состояние определяется предложением захвата.
Теперь подпись, которую нужно создать, не всегда является вашим выбором. Если вы передаете свою лямбду стандартному или стороннему API, то API требует, чтобы ваша лямбда имела определенную подпись. Если tgere - это какие-либо данные, которые вы хотите передать в дополнение к наложенной подписи, вам нужно захватить их.
Рассмотрим хорошо известный пример из библиотеки C: функция qsort.
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*));
Функция компаратора принимает указатели на два сравниваемых объекта и все. Невозможно передать дополнительный флаг, который бы контролировал, как именно выполняется сравнение. В качестве примера рассмотрим сортировку списка слов на некотором естественном языке в соответствии с правилами сопоставления этого языка, определенными во время выполнения. Как вы говорите своему компаратору, какой язык использовать? Единственный вариант с этим API - установить язык в статической переменной (yikes).
Из-за этого известного недостатка люди определяют различные нестандартные API замены. Например
void qsort_r(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *, void *),
void *arg);
Я надеюсь, что вы понимаете, что происходит. Вы передаете дополнительный аргумент (идентификатор языка или любой другой) как arg
затем функция сортировки передает его в виде запечатанного пакета вашему компаратору. Затем он преобразует аргумент в исходный тип и использует его.
Введите C++. В std::sort
Компаратор - это функция, подобная объекту, который несет свое собственное состояние. Так что этот трюк не нужен. Вы определяете что-то вроде
struct LanguageSensitiveComparator
{
LanguageSensitiveComparator(LangauageID lang) : lang(lang) {}
LangauageID lang;
bool operator()(const string& a, const string& b) const { .... } // etc
};
sort(dict.begin(), dict.end(), LanguageSensitiveComparator(lang));
C++11 делает еще один шаг вперед. Теперь вы можете определить объект функции на месте, используя лямбду.
sort (begin(dict), end(dict),
[=lang](const string& a, const string& b) { .. });
Вернемся к вашему вопросу. Не могли бы вы передать lang в качестве аргумента вместо того, чтобы захватить его? Конечно, но вам нужно определить собственную сортировку, которая знает о дополнительном параметре LabguageID (это то, что в основном делает qsort_r, за исключением того, что он не является безопасным по типу).
Разница в том, что один и тот же захват может использоваться с разными аргументами.
Рассмотрим следующий простой пример
#include <iostream>
#include <iterator>
#include <algorithm>
int main()
{
int a[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
const int N = 10;
for ( const auto &item : a ) std::cout << item << ' ';
std::cout << '\n';
std::transform( std::begin( a ), std::end( a ), std::begin( a ),
[=]( const auto &item ) { return N * item; } );
for ( const auto &item : a ) std::cout << item << ' ';
std::cout << '\n';
return 0;
}
Выход программы
0 1 2 3 4 5 6 7 8 9
0 10 20 30 40 50 60 70 80 90
Аргументы для лямбды предоставляются алгоритмом std::transform. Алгоритм не может передать лямбда-множитель N. Таким образом, вам нужно захватить его, и множитель будет использоваться с любым аргументом, переданным лямбда-выражению.