Может ли lambdas перевести на функции?
Общие знания диктуют, что лямбда-функции являются функторами под капотом.
В этом видео (@около 45:43) Бьярне говорит:
Я упомянул, что лямбда преобразуется в объект функции или в функцию, если удобная
Я вижу, как это оптимизация компилятора (т.е. это не меняет восприятие lambdas как неназванных функторов, что означает, что, например, lambdas все равно не будет перегружать), но существуют ли какие-либо правила, которые указывают, когда это применимо?
Изменить
То, как я понимаю термин переводить (и то, о чем я прошу) не имеет ничего общего с преобразованием (я не спрашиваю, можно ли конвертировать lambdas в функцию ptr и т.д.). По переводу я имею в виду "компилировать лямбда-выражения в функции вместо объектов-объектов".
Как упомянутый в cppreference:
Выражение лямбда строит неназванный временный объект prvalue уникального неназванного неединичного неагрегатного типа, известного как тип закрытия.
Вопрос в следующем: может ли этот объект быть пропущенным и иметь вместо него обычную функцию? Если да, то когда и как?
Примечание. Я предполагаю, что одно из таких правил "не захватывает ничего", но я не могу найти надежных источников для подтверждения.
Ответы
Ответ 1
TL;DR: если вы используете только lambda, чтобы преобразовать его в указатель на функцию (и только вызывать его с помощью этого указателя функции), всегда бывает выгодно опустить объект закрытия. Оптимизация, которая позволяет это, - это встраивание и удаление мертвого кода. Если вы используете сам лямбда, по-прежнему можно оптимизировать закрытие, но требует несколько более агрессивной межпроцедурной оптимизации.
Теперь я попытаюсь показать, как это работает под капотом. Я буду использовать GCC в своих примерах, потому что я больше знаком с этим. Другие компиляторы должны делать что-то подобное.
Рассмотрим этот код:
#include <stdio.h>
typedef int (* fnptr_t)(int);
void use_fnptr(fnptr_t fn)
{
printf("fn=%p, fn(1)=%d\n", fn, fn(1));
}
int main()
{
auto lam = [] (int x) { return x + 1; };
use_fnptr((fnptr_t)lam);
}
Теперь я скомпилирую его и выдаю промежуточное представление (для версий до 6, вы должны добавить -std=c++11
):
g++ test.cc -fdump-tree-ssa
Немного очищенный и отредактированный (для краткости) дамп выглядит следующим образом:
// _ZZ4mainENKUliE_clEi
main()::<lambda(int)> (const struct __lambda0 * const __closure, int x)
{
return x_1(D) + 1;
}
// _ZZ4mainENUliE_4_FUNEi
static int main()::<lambda(int)>::_FUN(int) (int D.2780)
{
return main()::<lambda(int)>::operator() (0B, _2(D));
}
// _ZZ4mainENKUliE_cvPFiiEEv
main()::<lambda(int)>::operator int (*)(int)() const (const struct __lambda0 * const this)
{
return _FUN;
}
int main() ()
{
struct __lambda0 lam;
int (*<T5c1>) (int) _3;
_3 = main()::<lambda(int)>::operator int (*)(int) (&lam);
use_fnptr (_3);
}
То есть, lambda имеет 2 функции-члена: оператор вызова функции и оператор преобразования и одну статическую функцию-член _FUN, которая просто вызывает lambda с this
, установленным на ноль. main
вызывает оператор преобразования и передает результат use_fnptr - точно так же, как он написан в исходном коде.
Я могу написать:
extern "C" int _ZZ4mainENKUliE_clEi(void *, int);
int main()
{
auto lam = [] (int x) { return x + 1; };
use_fnptr((fnptr_t)lam);
printf("%d %d %d\n", lam(10), _ZZ4mainENKUliE_clEi(&lam, 11), __lambda0::_FUN(12));
printf("%p %p\n", &__lambda0::_FUN, (fnptr_t)lam);
return 0;
}
Эта программа выводит:
fn=0x4005fc, fn(1)=2
11 12 13
0x4005fc 0x4005fc
Теперь, я думаю, это довольно очевидно, что компилятор должен включить lambda (_ZZ4mainENKUliE_clEi) в _FUN
(_ZZ4mainENUliE_4_FUNEi), потому что _FUN
является единственным вызывающим. И inline operator int (*)(int)
в main
(потому что этот оператор просто возвращает константу). GCC делает именно это при компиляции с оптимизацией (-O). Вы можете проверить это как:
g++ test.cc -O -fdump-tree-einline
Файл дампа:
// Considering inline candidate main()::<lambda(int)>.
// Inlining main()::<lambda(int)> into static int main():<lambda(int)>::_FUN(int).
static int main()::<lambda(int)>::_FUN(int) (int D.2822)
{
return _2(D) + 1;
}
Объект закрытия исчез. Теперь, более сложный случай, когда используется сама лямбда (не указатель функции). Рассмотрим:
#include <stdio.h>
#define PRINT(x) printf("%d", (x))
#define PRINT1(x) PRINT(x); PRINT(x); PRINT(x); PRINT(x);
#define PRINT2(x) do { PRINT1(x) PRINT1(x) PRINT1(x) PRINT1(x) } while(0)
__attribute__((noinline)) void use_lambda(auto t)
{
t(1);
}
int main()
{
auto lam = [] (int x) { PRINT2(x); };
use_lambda(lam);
return 0;
}
GCC не будет встраивать lambda, потому что он довольно большой (для этого я использовал printf):
g++ test2.cc -O2 -fdump-ipa-inline -fdump-tree-einline -fdump-tree-esra
Сброс раннего клина:
Considering inline candidate main()::<lambda(int)>
will not early inline: void use_lambda(auto:1) [with auto:1 = main()::<lambda(int)>]/16->main()::<lambda(int)>/19, growth 46 exceeds --param early-inlining-insns
Но пропуск "ранней межпроцедурной скалярной замены агрегатов" будет делать то, что мы хотим:
;; Function main()::<lambda(int)> (_ZZ4mainENKUliE_clEi, funcdef_no=14, decl_uid=2815, cgraph_uid=12, symbol_order=12)
IPA param adjustments: 0. base_index: 0 - __closure, base: __closure, remove_param
1. base_index: 1 - x, base: x, copy_param
Первый параметр (т.е. замыкание) не используется, и он удаляется. К сожалению, межпроцедурная SRA не может оптимизировать просадку, которая вводится для захваченных значений (хотя бывают случаи, когда это было бы очевидно выгодно), поэтому есть еще место для улучшений.
Ответ 2
Из лямбда-выражений §5.1.2 p6 (черновик N4140)
Тип замыкания для не-общего лямбда-выражения без лямбда-захвата имеет публичную не виртуальную не- явная функция преобразования const в указатель на функцию с языковой связью языка С++ с одинаковым параметром и возвращаемыми типами в качестве оператора вызова функции закрытия.
Ответ 3
Стандартная цитата уже опубликована, я хочу добавить несколько примеров.
Вы можете назначить lambdas для работы указателей, пока нет захваченных переменных:
Допустимо:
int (*f)(int) = [] (int x) { return x + 1; }; // assign lambda to function pointer
int z = f(3); // use the function pointer
Illegal:
int y = 5;
int (*g)(int) = [y] (int x) { return x + y; }; // error
Допустимо:
int y = 5;
int z = ([y] (int x) { return x + y; })(2); // use lambda directly
(Edit)
Поскольку мы не можем спросить Бьярне, что он имел в виду, я хочу попробовать несколько интерпретаций.
"translate" означает "convert"
Это то, что я понял изначально, но теперь ясно, что вопрос заключается не в этом возможном значении.
"переводить", как используется в стандарте С++, что означает "скомпилировать" (более или менее)
Как уже отмечал Себастьян Редл, на двоичном уровне нет функциональных объектов. Есть только коды операций и данных, и стандарт не говорит о каких-либо двоичных форматах и не указывает их.
"translate" означает "быть семантически эквивалентным"
Это означало бы, что если A и B семантически эквивалентны, полученный двоичный код для A и B может быть одним и тем же. Остальная часть моего ответа использует эту интерпретацию.
A закрытие состоит из двух частей:
- утверждения в лямбда-теле, "код"
- захваченные значения переменных или ссылки, "данные"
Это эквивалентно функтору, как уже указывалось в вопросе.
Функторы можно рассматривать как подмножество объектов, потому что у них есть код и данные, но только одна функция-член: оператор вызова. Таким образом, замыкания можно рассматривать как семантически эквивалентные ограниченной форме объектов.
A функция, с другой стороны, не имеет связанных с ней данных. Конечно, есть аргументы, но они должны предоставляться вызывающим и могут меняться от одного вызова к другому. Это семантическая разница с закрытием, где связанные переменные не могут быть изменены и не предоставляются вызывающим абонентом.
Функция-член не является чем-то независимым, поскольку она не может работать без его объекта, поэтому я думаю, что этот вопрос относится к автономной функции.
Так что нет, лямбда вообще не семантически эквивалентна функции.
Существует очевидный частный случай лямбда без захваченных переменных, где функтор состоит только из кода, и это эквивалентно функции.
Но, можно сказать, что лямбда семантически эквивалентна функции set. Каждое возможное закрытие (отличная комбинация значений/ссылок для связанных переменных) будет эквивалентно одной функции в этом наборе.
Конечно, это может быть полезно только тогда, когда связанные переменные могут иметь только очень ограниченный набор значений /, являются ссылками только на несколько разных переменных, если вообще.
Например, я не вижу причин, по которым компилятор не мог рассматривать следующие два фрагмента как эквивалент (почти *):
void Test(bool cond, int x)
{
int y;
if(cond) y = 5;
else y = 3;
auto f = [y](int x) { return x + y; };
// more code that
// uses f
}
Умный компилятор мог видеть, что y может иметь только значения 5 или 3 и компилировать, как если бы он был написан следующим образом:
int F1(int x)
{
return x + 5;
}
int F2(int x)
{
return x + 3;
}
void Test(bool cond, int x)
{
int (*f)(int);
if(cond) f = F1;
else f = F2;
// more code that
// uses f
}
(*) Конечно, это зависит от того, что именно делает more code that uses f
.
Другим (может быть, лучшим) примером будет лямбда, которая всегда связывает одну и ту же переменную по ссылке. Тогда существует только одно возможное замыкание, и поэтому оно эквивалентно функции, если функция имеет доступ к этой переменной другими способами, чем передавая ее в качестве аргумента.
Другим замечанием, которое может быть полезно, является запрос
может ли этот объект [замыкание] быть пропущенным и иметь обычную функцию? Если да, то когда и как?
более или менее совпадает с запросом, когда и как функция-член может использоваться без объекта. Поскольку лямбды являются функторами, а функторы - объектами, эти два вопроса тесно связаны.
Связанные переменные лямбда соответствуют элементам данных объекта, а тело лямбда соответствует телу функции-члена.
Ответ 4
Чтобы дать еще один вид проницательности, давайте взглянем на код, созданный clang при компиляции следующего фрагмента:
int (*f) = []() { return 0; }
Если вы скомпилируете это с помощью:
clang++ -std=c++11 -S -o- -emit-llvm a.cc
Для определения лямбда вы получаете следующий байт-код LLVM:
define internal i32 @"_ZNK3$_0clEv"(%class.anon* %this) #0 align 2 {
%1 = alloca %class.anon*, align 8
store %class.anon* %this, %class.anon** %1, align 8
%2 = load %class.anon** %1
ret i32 0
}
define internal i32 @"_ZN3$_08__invokeEv"() #1 align 2 {
%1 = call i32 @"_ZNK3$_0clEv"(%class.anon* undef)
ret i32 %1
}
Первая функция принимает экземпляр %class.anon*
и возвращает 0: этот оператор вызова. Второй создает экземпляр этого класса (undef
), а затем вызывает его оператор вызова и возвращает значение.
Когда скомпилировано с -O2
, вся лямбда превращается в:
define internal i32 @"_ZN3$_08__invokeEv"() #0 align 2 {
ret i32 0
}
Итак, для одной функции, которая возвращает 0.
Я упомянул, что лямбда преобразуется в объект функции или в функцию, если удобная
То, что делает clang! Он преобразует лямбда в объект функции и, если возможно, оптимизирует его для функции.
Ответ 5
Нет, это невозможно. Lambdas определены как функторы, и я не вижу здесь правила as-if.
[C++14: 5.1.2/6]:
Тип замыкания для не-общего лямбда-выражения без лямбда-захвата имеет публичную не виртуальную неявную функцию преобразования const в указатель на функцию с С++ языковой связью (7.5) с тем же параметром и возвращаемые типы, как оператор вызова функции закрытия. [..]
& hellip, за которым следуют аналогичные формулировки для родовых лямбдов.