Это то, что меня всегда беспокоило как особенность лямбда-выражений C ++: тип лямбда-выражения C ++ уникален и анонимен, я просто не могу его записать. Даже если я создам две лямбда-выражения, которые синтаксически совершенно одинаковы, результирующие типы определены как разные. Следствием этого является то, что а) лямбда-выражения могут быть переданы только шаблонным функциям, которые позволяют передавать вместе с объектом время компиляции, невыразимый тип, и б) лямбда-выражения полезны только после того, как они были стерты через тип std::function<>
.
Хорошо, но именно так это делает C ++, я был готов списать это на утомительную особенность этого языка. Однако я только что узнал, что Rust, похоже, делает то же самое: каждая функция или лямбда Rust имеет уникальный анонимный тип. А теперь мне интересно: почему?
Итак, у меня такой вопрос: в
чем преимущество с точки зрения разработчика языка введение в язык концепции уникального анонимного типа?
источник
std::function
. Лямбда, переданная в шаблонную функцию, может быть вызвана напрямую без участияstd::function
. Затем компилятор может встроить лямбда в функцию шаблона, что повысит эффективность выполнения.{ int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }
поскольку переменная, на которую оно ссылается, отличается, даже если текстуально они одинаковы. Если вы просто скажете, что все они уникальны, вам не нужно беспокоиться о попытках понять это.lambdas_type = decltype( my_lambda);
[](auto) {}
? Следует ли для начала иметь тип?Ответы:
Многие стандарты (особенно C ++) сводятся к минимуму того, сколько они требуют от компиляторов. Честно говоря, они уже достаточно требуют! Если им не нужно указывать что-то, чтобы это работало, они, как правило, оставляют реализацию определенной.
Если бы лямбды не были анонимными, нам пришлось бы их определять. Это должно многое сказать о том, как фиксируются переменные. Рассмотрим случай лямбды
[=](){...}
. Тип должен указывать, какие типы фактически были захвачены лямбдой, что может быть нетривиальной задачей для определения. Кроме того, что, если компилятор успешно оптимизирует переменную? Рассмотреть возможность:static const int i = 5; auto f = [i]() { return i; }
Оптимизирующий компилятор может легко распознать, что единственное возможное значение,
i
которое может быть захвачено, - 5, и заменить его наauto f = []() { return 5; }
. Однако, если тип не анонимный, это может изменить тип или заставить компилятор меньше оптимизировать, сохраняя,i
даже если на самом деле это не нужно. Это целый набор сложностей и нюансов, которые просто не нужны для того, для чего предназначены лямбды.А если вам действительно нужен неанонимный тип, вы всегда можете создать закрывающий класс самостоятельно и работать с функтором, а не с лямбда-функцией. Таким образом, они могут заставить лямбды обрабатывать случай 99%, а вам оставят кодировать собственное решение в 1%.
Дедупликатор указал в комментариях, что я обращаю внимание не столько на уникальность, сколько на анонимность. Я менее уверен в преимуществах уникальности, но стоит отметить, что поведение следующего ясно, если типы уникальны (действие будет создано дважды).
int counter() { static int count = 0; return count++; } template <typename FuncT> void action(const FuncT& func) { static int ct = counter(); func(ct); } ... for (int i = 0; i < 5; i++) action([](int j) { std::cout << j << std::endl; }); for (int i = 0; i < 5; i++) action([](int j) { std::cout << j << std::endl; });
Если бы типы не были уникальными, нам пришлось бы указать, какое поведение должно происходить в этом случае. Это может быть сложно. Некоторые из вопросов, которые были подняты в связи с темой анонимности, также поднимают свою уродливую голову в этом случае для уникальности.
источник
Лямбды - это не просто функции, это функция и состояние . Поэтому и C ++, и Rust реализуют их как объект с оператором вызова (
operator()
в C ++ - 3Fn*
черты в Rust).В принципе,
[a] { return a + 1; }
в десахарах C ++ что-то вродеstruct __SomeName { int a; int operator()() { return a + 1; } };
затем используя экземпляр того,
__SomeName
где используется лямбда.Находясь в Rust,
|| a + 1
в Rust будет десахарироваться до чего-то вроде{ struct __SomeName { a: i32, } impl FnOnce<()> for __SomeName { type Output = i32; extern "rust-call" fn call_once(self, args: ()) -> Self::Output { self.a + 1 } } // And FnMut and Fn when necessary __SomeName { a } }
Это означает, что большинство лямбд должны иметь разные типы.
Есть несколько способов сделать это:
Fn*
трейтами в Rust. Ни один из языков никогда не заставляет вас стирать типы лямбда-выражений, чтобы использовать их (std::function
в C ++ илиBox<Fn*>
Rust).Также обратите внимание, что оба языка согласны с тем, что тривиальные лямбда-выражения, не захватывающие контекст, могут быть преобразованы в указатели на функции.
Описание сложных функций языка с помощью более простых функций довольно распространено. Например, и C ++, и Rust имеют циклы range-for, и оба описывают их как синтаксический сахар для других функций.
C ++ определяет
for (auto&& [first,second] : mymap) { // use first and second }
как эквивалент
{ init-statement auto && __range = range_expression ; auto __begin = begin_expr ; auto __end = end_expr ; for ( ; __begin != __end; ++__begin) { range_declaration = *__begin; loop_statement } }
а Rust определяет
for <pat> in <head> { <body> }
как эквивалент
let result = match ::std::iter::IntoIterator::into_iter(<head>) { mut iter => { loop { let <pat> = match ::std::iter::Iterator::next(&mut iter) { ::std::option::Option::Some(val) => val, ::std::option::Option::None => break }; SemiExpr(<body>); } } };
которые кажутся более сложными для человека, но более простыми для разработчика языков или компилятора.
источник
std::function
делает этоstd::function
(Добавление к ответу Калет, но слишком длинное, чтобы поместиться в комментарии.)
Лямбда-выражение - это просто синтаксический сахар для анонимной структуры (типа Волан-де-Морта, потому что вы не можете произнести его имя).
Вы можете увидеть сходство между анонимной структурой и анонимностью лямбда-выражения в этом фрагменте кода:
#include <iostream> #include <typeinfo> using std::cout; int main() { struct { int x; } foo{5}; struct { int x; } bar{6}; cout << foo.x << " " << bar.x << "\n"; cout << typeid(foo).name() << "\n"; cout << typeid(bar).name() << "\n"; auto baz = [x = 7]() mutable -> int& { return x; }; auto quux = [x = 8]() mutable -> int& { return x; }; cout << baz() << " " << quux() << "\n"; cout << typeid(baz).name() << "\n"; cout << typeid(quux).name() << "\n"; }
Если это все еще неудовлетворительно для лямбды, это должно быть также неудовлетворительным для анонимной структуры.
Некоторые языки допускают более гибкий вид утиной печати, и даже несмотря на то, что в C ++ есть шаблоны, которые на самом деле не помогают в создании объекта из шаблона, который имеет поле члена, которое может заменить лямбда напрямую, а не с использованием
std::function
обертка.источник
int& operator()(){ return x; }
к этим структурамauto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }
, потому чтоfoo
ничто не может использовать идентификаторDarkLord
Потому что есть случаи, когда имена неуместны, бесполезны или даже контрпродуктивны. В этом случае способность абстрагироваться от их существования полезна, потому что она уменьшает загрязнение имен и решает одну из двух сложных проблем в информатике (как называть вещи). По той же причине полезны временные объекты.
Уникальность - это не особая лямбда-функция или даже особенность анонимных типов. Это также применимо к именованным типам в языке. Учтите следующее:
struct A { void operator()(){}; }; struct B { void operator()(){}; }; void foo(A);
Обратите внимание, что я не могу перейти
B
вfoo
, даже если классы одинаковы. Это же свойство применимо к безымянным типам.Есть третий вариант для подмножества лямбда-выражений: лямбда-выражения без захвата могут быть преобразованы в указатели на функции.
Обратите внимание, что если ограничения анонимного типа являются проблемой для варианта использования, решение простое: вместо этого можно использовать именованный тип. Лямбды не делают ничего, чего нельзя было бы сделать с именованным классом.
источник
Принятый ответ Корта Аммона хорош, но я думаю, что есть еще один важный момент, касающийся реализуемости.
Предположим, у меня есть две разные единицы перевода: one.cpp и two.cpp.
// one.cpp struct A { int operator()(int x) const { return x+1; } }; auto b = [](int x) { return x+1; }; using A1 = A; using B1 = decltype(b); extern void foo(A1); extern void foo(B1);
Две перегрузки
foo
используют один и тот же идентификатор (foo
), но имеют разные искаженные имена. (В Itanium ABI, используемом в системах типа POSIX, искаженные имена -_Z3foo1A
и, в данном конкретном случае,._Z3fooN1bMUliE_E
)// two.cpp struct A { int operator()(int x) const { return x + 1; } }; auto b = [](int x) { return x + 1; }; using A2 = A; using B2 = decltype(b); void foo(A2) {} void foo(B2) {}
Компилятор C ++ должен гарантировать, что искаженное имя
void foo(A1)
в "two.cpp" совпадает с искаженным именемextern void foo(A2)
в "one.cpp", чтобы мы могли связать два объектных файла вместе. Это физический смысл того, что два типа являются «одним и тем же типом»: по сути, речь идет о совместимости с ABI между отдельно скомпилированными объектными файлами.Компилятор C ++ не обязан гарантировать, что
B1
иB2
являются «одного типа». (Фактически, необходимо убедиться, что это разные типы, но сейчас это не так важно.)Какой физический механизм использует компилятор, чтобы гарантировать, что
A1
иA2
являются «одного типа»?Он просто копается в определениях типов, а затем смотрит на полное имя типа. Это тип класса с именем
A
. (Ну,::A
поскольку он находится в глобальном пространстве имен.) Таким образом, это один и тот же тип в обоих случаях. Это легко понять. Что еще более важно, это легко реализовать . Чтобы увидеть, являются ли два типа классов одним и тем же типом, вы берете их имена и выполняетеstrcmp
. Чтобы преобразовать тип класса в искаженное имя функции, вы пишете количество символов в его имени, за которым следуют эти символы.Итак, именованные типы легко подделать.
Какой физический механизм мог бы использовать компилятор, чтобы гарантировать, что
B1
иB2
являются «одного типа» в гипотетическом мире, где C ++ требует, чтобы они были одного типа?Ну, он не мог использовать имя типа, так как тип не имеет имени.
Возможно, он мог как-то закодировать текст тела лямбды. Но это было бы немного неудобно, потому что на самом деле
b
in "one.cpp" немного отличается отb
in "two.cpp": "one.cpp" имеет,x+1
а "two.cpp" имеетx + 1
. Таким образом, мы должны были бы придумать правило, которое гласит, что либо эта разница в пробелах не имеет значения, либо она имеет значение (в конце концов, делая их разными типами), либо, возможно, имеет значение (возможно, действительность программы определяется реализацией , или, может быть, это «плохо сформировано, диагностика не требуется»). Тем не мение,A
Самый простой выход из затруднения - просто сказать, что каждое лямбда-выражение производит значения уникального типа. Тогда два лямбда-типа, определенные в разных единицах перевода, определенно не являются одним и тем же типом . В рамках одной единицы трансляции мы можем «давать имена» лямбда-типам, просто считая от начала исходного кода:
auto a = [](){}; // a has type $_0 auto b = [](){}; // b has type $_1 auto f(int x) { return [x](int y) { return x+y; }; // f(1) and f(2) both have type $_2 } auto g(float x) { return [x](int y) { return x+y; }; // g(1) and g(2) both have type $_3 }
Конечно, эти имена имеют значение только в пределах этой единицы перевода. Этот TU
$_0
всегда отличается от некоторых других TU$_0
, даже если этот TUstruct A
всегда того же типа, что и некоторые другие TUstruct A
.Кстати, обратите внимание , что наш «кодировать текст лямбды» идея было еще одна тонкой проблемы: лямбды
$_2
и$_3
состоит из точно такого же текста , но они не должны четко рассматриваться тем же типа!Между прочим, C ++ требует, чтобы компилятор знал, как искажать текст произвольного выражения C ++ , как в
template<class T> void foo(decltype(T())) {} template void foo<int>(int); // _Z3fooIiEvDTcvT__EE, not _Z3fooIiEvT_
Но C ++ (пока) не требует, чтобы компилятор знал, как искажать произвольный оператор C ++ .
decltype([](){ ...arbitrary statements... })
все еще плохо сформирован даже в C ++ 20.Также обратите внимание, что безымянному типу легко присвоить локальный псевдоним с помощью
typedef
/using
. У меня такое чувство, что ваш вопрос мог возникнуть из-за попытки сделать что-то, что можно было бы решить подобным образом.auto f(int x) { return [x](int y) { return x+y; }; } // Give the type an alias, so I can refer to it within this translation unit using AdderLambda = decltype(f(0)); int of_one(AdderLambda g) { return g(1); } int main() { auto f1 = f(1); assert(of_one(f1) == 2); auto f42 = f(42); assert(of_one(f42) == 43); }
ИЗМЕНИТЬ ДОБАВИТЬ: Читая некоторые из ваших комментариев к другим ответам, похоже, вам интересно, почему
int add1(int x) { return x + 1; } int add2(int x) { return x + 2; } static_assert(std::is_same_v<decltype(add1), decltype(add2)>); auto add3 = [](int x) { return x + 3; }; auto add4 = [](int x) { return x + 4; }; static_assert(not std::is_same_v<decltype(add3), decltype(add4)>);
Это потому, что лямбды без захвата могут быть сконструированы по умолчанию. (В C ++ только с C ++ 20, но концептуально это всегда было верно.)
template<class T> int default_construct_and_call(int x) { T t; return t(x); } assert(default_construct_and_call<decltype(add3)>(42) == 45); assert(default_construct_and_call<decltype(add4)>(42) == 46);
Если бы вы попробовали
default_construct_and_call<decltype(&add1)>
,t
это будет указатель на функцию, инициализированный по умолчанию, и вы, вероятно, будете segfault. Это вроде бесполезно.источник
Лямбда-выражения C ++ нуждаются в разных типах для различных операций, поскольку C ++ связывает статически. Их можно только копировать / перемещать, поэтому в большинстве случаев вам не нужно указывать их тип. Но это все детали реализации.
Я не уверен, есть ли у лямбда-выражений C # тип, поскольку они являются «выражениями анонимных функций» и сразу же преобразуются в совместимый тип делегата или тип дерева выражений. Если да, вероятно, это непроизносимый тип.
В C ++ также есть анонимные структуры, в которых каждое определение приводит к уникальному типу. Здесь имя не является непроизносимым, оно просто не существует в соответствии со стандартом.
В C # есть анонимные типы данных , которым строго запрещено выходить за пределы определенной области. Реализация даёт и им уникальное непроизносимое имя.
Наличие анонимного типа сигнализирует программисту о том, что он не должен ковыряться в своей реализации.
В сторону:
Вы можете дать имя лямбда-типу.
auto foo = []{}; using Foo_t = decltype(foo);
Если у вас нет захватов, вы можете использовать тип указателя функции
void (*pfoo)() = foo;
источник
Foo_t = []{};
, толькоFoo_t = foo
и ничего больше.Зачем использовать анонимные типы?
Для типов, которые автоматически генерируются компилятором, можно выбрать либо (1) удовлетворить запрос пользователя на имя типа, либо (2) позволить компилятору выбрать одно из них самостоятельно.
В первом случае ожидается, что пользователь будет явно указывать имя каждый раз, когда появляется такая конструкция (C ++ / Rust: всякий раз, когда определена лямбда; Rust: всякий раз, когда определяется функция). Это утомительная деталь, которую пользователь каждый раз сообщать, и в большинстве случаев имя никогда не упоминается снова. Таким образом, имеет смысл позволить компилятору автоматически определять имя для него и использовать существующие функции, такие как
decltype
или вывод типа, для ссылки на тип в тех немногих местах, где это необходимо.В последнем случае компилятор должен выбрать уникальное имя для типа, которое, вероятно, будет неясным, нечитаемым именем, например
__namespace1_module1_func1_AnonymousFunction042
. Разработчик языка мог точно указать, как это имя сконструировано в великолепных и деликатных деталях, но это излишне раскрывает пользователю деталь реализации, на которую не может полагаться ни один здравомыслящий пользователь, поскольку имя, без сомнения, хрупкое перед лицом даже незначительных рефакторов. Это также излишне ограничивает развитие языка: будущие добавления функций могут вызвать изменение существующего алгоритма генерации имен, что приведет к проблемам обратной совместимости. Таким образом, имеет смысл просто опустить эту деталь и заявить, что автоматически сгенерированный тип не может быть произнесен пользователем.Зачем использовать уникальные (разные) типы?
Если значение имеет уникальный тип, то оптимизирующий компилятор может отслеживать уникальный тип на всех сайтах его использования с гарантированной точностью. Как следствие, пользователь может быть уверен в том, что компилятору полностью известно происхождение этого конкретного значения.
Например, в тот момент, когда компилятор видит:
let f: __UniqueFunc042 = || { ... }; // definition of __UniqueFunc042 (assume it has a nontrivial closure) /* ... intervening code */ let g: __UniqueFunc042 = /* some expression */; g();
компилятор полностью уверен в том, что он
g
обязательно должен происходитьf
, даже не зная происхожденияg
. Это позволитg
девиртуализировать вызов. Пользователь тоже должен это знать, поскольку он очень позаботился о сохранении уникального типаf
сквозного потока данных, к которому он привелg
.Обязательно это ограничивает то, что может делать пользователь
f
. Пользователь не вправе писать:let q = if some_condition { f } else { || {} }; // ERROR: type mismatch
поскольку это привело бы к (незаконному) объединению двух различных типов.
Чтобы обойти это, пользователь может преобразовать
__UniqueFunc042
значение в неуникальный тип&dyn Fn()
,let f2 = &f as &dyn Fn(); // upcast let q2 = if some_condition { f2 } else { &|| {} }; // OK
Компромисс, связанный с этим стиранием типа, заключается в том, что использование оператора
&dyn Fn()
усложняет рассуждение для компилятора. Дано:let g2: &dyn Fn() = /*expression */;
компилятор должен тщательно изучить,
/*expression */
чтобы определить,g2
происходит ли это изf
какой-либо другой функции (функций), а также условия, при которых выполняется это происхождение. Во многих случаях компилятор может сдаться: возможно, человек сможет сказать, что этоg2
действительно происходитf
во всех ситуациях, кроме пути отf
кg2
был слишком запутанным, чтобы компилятор мог его расшифровать, что приводило к виртуальному вызовуg2
с пессимистической производительностью.Это становится более очевидным, когда такие объекты передаются универсальным (шаблонным) функциям:
fn h<F: Fn()>(f: F);
Если звонить
h(f)
кудаf: __UniqueFunc042
, тоh
специализируется на уникальном экземпляре:Это позволяет компилятору генерировать специализированный код для
h
, адаптированный для конкретного аргументаf
, и отправить его вf
скорее всего, будет статической, если не встроенной.В противоположном сценарии, когда звонят
h(f)
сf2: &Fn()
,h
создается экземпляр какh::<&Fn()>(f);
который является общим для всех функций типа
&Fn()
. Изнутриh
компилятор очень мало знает о непрозрачной функции типа&Fn()
и поэтому может вызывать только консервативноf
с помощью виртуальной отправки. Для статической диспетчеризации компилятор должен будет встроить вызовh::<&Fn()>(f)
на свой сайт вызова, что не гарантируется, еслиh
он слишком сложен.источник
void(*)(int, double)
может не быть имени, но я могу его записать. Я бы назвал это безымянным типом, а не анонимным типом. И я бы назвал такие загадочные вещи, как__namespace1_module1_func1_AnonymousFunction042
искажение имен, что определенно выходит за рамки этого вопроса. Этот вопрос касается типов, которые, как гарантируется стандартом, невозможно записать, в отличие от введения синтаксиса типов, который может выражать эти типы полезным способом.Во-первых, лямбда-выражения без захвата можно преобразовать в указатель на функцию. Таким образом, они обеспечивают некоторую универсальность.
Теперь почему лямбды с захватом не преобразовываются в указатель? Поскольку функция должна иметь доступ к состоянию лямбда-выражения, это состояние должно отображаться как аргумент функции.
источник
std::function<>
.Чтобы избежать конфликта имен с кодом пользователя.
Даже две лямбды с одинаковой реализацией будут иметь разные типы. Это нормально, потому что у меня тоже могут быть разные типы объектов, даже если их расположение в памяти одинаково.
источник
int (*)(Foo*, int, double)
, не подвергается риску столкновения имени с кодом пользователя.void(*)(void)
кvoid*
стандартному C / C ++ и обратно.