#include <iostream>
int foo(int x = [](){ static int x = 0; return ++x; }()) {
return x;
};
int main() {
std::cout << foo() << foo(); // prints "12", not "11"
}
Я знаю, что аргументы по умолчанию оцениваются каждый раз при вызове функции. Означает ли это, что тип лямбды при каждом вызове разный? Пожалуйста, укажите на стандартные цитаты, объясняющие поведение здесь.
тип каждого лямбда-выражения различен. Одно и то же выражение, вычисленное несколько раз, даст один и тот же тип. рассмотрим лямбду, определенную в цикле for. выражение оценивается на каждой итерации, но тип лямбда остается прежним.
@HolyBlackCat Лучше обман: Каков тип лямбды, выведенной с помощью «auto» в C++11?
@NathanOliver, можешь ли ты привести цитату, объясняющую это? Я думал, что лямбда-выражение описывает уникальный тип, а это означает, что даже если оно синтаксически эквивалентно предыдущему вычислению, оно будет другим.
@user12002570 user12002570 Я не понимаю, как эти ответы объясняют поведение моего вопроса.
@cppbest Да. Посмотрите эту демонстрацию
@cppbest Смотрите каждый раз, когда будет создан новый тип класса, как описано в моем ответе ниже. Демо cppinsights
Реальный вопрос не в том, почему это создает только один тип лямбды, а в том, почему использование лямбды в качестве аргумента шаблона по умолчанию каждый раз создает новую лямбду.
Анализ CPP показывает, что он не будет работать должным образом: cppinsights.io/s/7ca32971
Это CWG 2300 , как объяснено в ответе
@HolyBlackCat этот имеет отношение к твоему вопросу
5 хорошо документированных ответов, но, ИМХО, подведение итогов для простых смертных было бы интересно: - правильно ли, в конечном итоге, поведение, наблюдаемое опом? - какой на самом деле применяется набор правил (кажется, разные части стандарта упоминались в разных ответах, и, честно говоря, я теряю нить?
Вы можете получить 11
результат, преобразуя функцию в шаблон функции: godbolt.org/z/3vhhKGnEe
@cppbest «Меня интересуют стандартные вопросы или вопросы cwg» Это CWG2300, как описано здесь
@Alex iiuc, решение проблемы уже включено в стандарт, поэтому теперь можно будет ссылаться на точные цитаты для объяснения.
@cppbest См. часть моего ответа, в которой говорится: «лямбда-выражения, появляющиеся в аргументе D по умолчанию, могут по-прежнему обозначать разные типы в разных единицах перевода». Это должно прояснить ситуацию. Я также дал ссылку на него Basic.def.odr в ответе.
@user12002570 user12002570 Я думаю, что примечания и примеры не являются нормативными; к тому же эти конкретные цитаты касаются случая множественных ТУ, а у нас только один
@cppbest Да, примечания не являются нормативными. См. добавленную нормативную часть в ответе, в которой говорится: «Для каждой такой сущности и для самого D поведение такое, как если бы существовала одна сущность с одним определением».
@cppbest Ключ (нормативная формулировка, которую вы искали) здесь — Basic.def.odr , который объясняет поведение вашей программы и был добавлен в мой ответ. Это делает вывод 12
правильным. По сути, у вас есть невстроенная нешаблонная функция, которая может быть определена только в одном TU. Это означает, что может быть только одно определение самой лямбды. Это вместе со старой частью ответа проясняет ситуацию.
Это CWG 2300 , и поведение программы можно объяснить с помощью Basic.def.odr#15.6 и Basic.def.odr, что означает, что вывод 12
правильный.
- Для любого определяемого элемента D с определениями в нескольких единицах перевода:
- 15.1. ...
- 15.2. ...
программа плохо сформирована; диагностика требуется только в том случае, если определяемый элемент прикреплен к именованному модулю и предыдущее определение доступно в той точке, где происходит более позднее определение. Учитывая такой элемент, для всех определений D или, если D является неименованным перечислением, для всех определений D, которые достижимы в любой заданной точке программы, должны удовлетворяться следующие требования.
15.3. ...
15.4. ...
15.5. ...
15.6. В каждом таком определении, за исключением аргументов по умолчанию и аргументов шаблона по умолчанию для D, соответствующие лямбда-выражения должны иметь один и тот же тип замыкания (см. ниже).
Обратите внимание на приведенное выше «исключение» для аргументов по умолчанию D
. Что еще более важно, обратите внимание, что исключение не применимо к вашему примеру, потому что в вашем примере foo
— это невстроенная нешаблонная функция, что означает, что она может быть определена только в единице перевода. Это, в свою очередь, означает, что существует только одно определение самой лямбды и, следовательно, в вашем примере существует только один уникальный тип замыкания.
Это также можно увидеть из Basic.def.odr:
Если D является шаблоном и определен более чем в одной единице трансляции, то предыдущие требования должны применяться как к именам из охватывающей шаблон области, используемой в определении шаблона, так и к зависимым именам в момент создания экземпляра ([temp.dep ]). Эти требования также применяются к соответствующим объектам, определенным в каждом определении D (включая типы замыкания лямбда-выражений, но исключая объекты, определенные в аргументах по умолчанию или аргументах шаблона по умолчанию либо D, либо объекта, не определенного в D). Для каждой такой сущности и для самого D поведение такое, как если бы существовала одна сущность с единственным определением, в том числе и в применении этих требований к другим сущностям.
[Примечание 4: Сущность по-прежнему объявляется в нескольких единицах перевода, и [basic.link] по-прежнему применяется к этим объявлениям. В частности, лямбда-выражения ([expr.prim.lambda]), появляющиеся в типе D, могут привести к тому, что разные объявления будут иметь разные типы, а лямбда-выражения, появляющиеся в аргументе D по умолчанию, могут по-прежнему обозначать разные типы в разных переводах. единицы измерения. — последнее примечание]
[Пример 6:
inline void f(bool cond, void (*p)()) { if (cond) f(false, []{}); } inline void g(bool cond, void (*p)() = []{}) { if (cond) g(false); } struct X { void h(bool cond, void (*p)() = []{}) { if (cond) h(false); } };
Если определение
g
встречается в нескольких единицах трансляции, программа имеет неверный формат (диагностика не требуется), поскольку каждое такое определение использует аргумент по умолчанию, который ссылается на отдельный тип замыкания лямбда-выражения. Определение X может встречаться в нескольких единицах перевода действительной программы; лямбда-выражения, определенные в аргументе по умолчанию X::h в определении X, обозначают один и тот же тип замыкания в каждой единице перевода. — конец примера]
Отсюда также приходим к тому же выводу, что в вашем примере определение функции foo
встречается только в одной единице перевода. Данная программа правильно сформирована, и в вашем примере есть только один тип замыкания (в одном TU), поэтому вывод 12
верен в соответствии с текущей формулировкой.
Итак, если тип каждый раз разный, почему на выходе получается «12», а не «11»? Это ошибка?
@cppbest «Это ошибка?...» Выглядит так и детали реализации. Хотя вывод должен соответствовать поведению, которого здесь нет.
К вашему сведению: gcc, clang, MSVC согласны с тем, что 12 — правильный результат — в прямом эфире — godbolt.org/z/5vYPWxKnM
@cppbest Я добавил надуманный пример, который должен прояснить ситуацию. По сути, обе эти программы (ваша и моя) должны выйти из строя по одной и той же причине.
@RichardCritten Технически это не означает, что вывод правильный. Например, все компиляторы также принимают эту некорректную программу. Таким образом, согласие на один и тот же вывод не делает программу правильной. Я также видел много других примеров, когда все компиляторы принимают какую-то некорректную программу или выдают неправильный вывод.
Я не понимаю, как абзац «оценивается каждый раз» что-либо доказывает. auto f() { return []{}; }
также каждый раз оценивает лямбду, и это определенно не каждый раз другой тип.
@PasserBy Насколько я понимаю, это ошибка/недостаток стандарта. И согласно текущей редакции программа должна вывести 11
. Прежде чем пытаться проверить ситуацию примерами/аналогиями, вам следует сначала предоставить какое-то стандартное предложение, объясняющее поведение.
@PasserBy Спросите себя, является ли [](){//...}
аргументом по умолчанию. Да, это. Тогда согласно [dcl.fct.default] здесь он будет оцениваться дважды. Теперь спросите себя, является ли тип некоторых заданных лямбда-выражений уникальным. Да, согласно [expr.prim.lambda.closure] это так. Я хочу сказать, что стандарт мог бы прояснить это. То есть есть разница между тем, что должно быть и тем, что есть на данный момент по стандарту.
Он будет оценен дважды, но это ничего не говорит о его типе, см. пример в предыдущем комментарии.
@PasserBy, который ничего не говорит о его типе... Чтобы получить тип, необходима оценка. Как я уже сказал, это стандартный недостаток.
Пришло время подать DR.
@Red.Wave Да, у меня уже есть.
@user12002570 user12002570 дело не в природе лямбды, а в природе аргумента функции по умолчанию. В той же области выражение аргумента по умолчанию является уникальной сущностью и не может быть изменено явно или неявно, даже если вы можете переопределить выражение значения по умолчанию во вложенной области. Это делает невозможным создание нового лямбда-типа без захвата, независимого от параметров шаблона на каждом сайте вызова.
@Swift-FridayPie Да, именно поэтому я предложил добавить «во время определения (не во время оценки)» в упомянутую проблему cwg, поскольку это сделает это более понятным.
@user12002570 user12002570, и это все равно оставит крайний случай создания экземпляра шаблона и общей лямбды. Вероятно, потребуется разъяснение лямбда-выражения, используемого в качестве значения по умолчанию в глобальной области, вложенной области и области шаблона.
«должно произойти сбой по той же причине, что и...» Этот пример отличается тем, что вы пишете лямбду дважды.
Ошибка отсутствует. Лямбда объявляется один раз, существует только один тип лямбды, он вычисляется много раз.
@Gene Вот почему предложено изменение в проблеме cwg.
@Gene Вот как работает языковой ответ.
@user12002570 user12002570 эта проблема была открыта 2 часа назад. Дайте cwg немного времени, чтобы решить проблему как «уже работает».
@HolyBlackCat Я так и предполагал, да
iiuc, цитата про шаблоны, но в вопросе нет шаблона
@cppbest Только первая часть/предложение (которое не выделено) касается шаблонов. Остальное (выделено) относится и к нешаблонным. Вот почему примечание и пример нешаблонов приведены чуть ниже.
Вы правы в том, что каждое лямбда-выражение связано с уникальным типом замыкания, однако тип определяет само выражение, а не то, сколько раз оно вычисляется.
Поскольку мы говорим о лямбда-выражении, мы имеем дело с prvalue. Оценка prvalue инициализирует объект, имеющий ровно один тип. [expr.prim.lambda.closure] [dcl.fnc.default] [basic.lval] [intro.object]
Лямбда-выражение — это prvalue, объект результата которого называется объектом замыкания.
Тип лямбда-выражения (который также является типом объекта замыкания) представляет собой уникальный безымянный тип класса без объединения, называемый типом замыкания, свойства которого описаны ниже.
Аргумент по умолчанию оценивается каждый раз, когда функция вызывается без аргумента для соответствующего параметра.
prvalue — это выражение, оценка которого инициализирует объект или вычисляет значение операнда оператора, как указано в контексте, в котором оно появляется, или выражение, имеющее тип cv void.
Свойства объекта определяются при его создании. Объект может иметь имя. Объект имеет продолжительность хранения, которая влияет на его срок службы. У объекта есть тип.
Выражение остается одного и того же типа на протяжении всего выполнения [defns.static.type]
статический тип
тип выражения, полученный в результате анализа программы без учета семантики выполнения
12 — правильный результат для вашего примера.
Вопрос лингвистический. Весь смысл в том, чтобы найти стандартную ссылку/предложение. Часто все три компилятора принимают неправильно сформированную или неправильную программу.
@user12002570 user12002570 на самом деле очень сложно найти стандартную цитату о том, что «выражение остается одного и того же типа на протяжении всего выполнения»
Это задача.
Цитаты не подтверждают ваш вывод. Вы только что добавили цитирование, чтобы этот ответ выглядел как ответ лингвиста.
Я согласен, что это правильный ответ, но если вы собираетесь цитировать те же цитаты, что и в другом ответе, я предлагаю объяснить, почему ваша интерпретация верна, а их — нет.
Как одни и те же цитаты могут означать разные/противоположные вещи. Стандарт нуждается в некоторой модификации, чтобы прояснить это, как указано в представленном выпуске cwg.
Отредактированный ответ противоречит выводу этого ответа?
@user12002570 user12002570 нет, вы просто совершенно не понимаете определение и оценку.
@Калет Нет, я точно знаю, что они имеют в виду. Вот почему предложенное изменение в выпуске cwg. Отредактированный ответ по-прежнему ничего не объясняет по заданному вопросу. ОП уже знает, что объект имеет тип, а лямбда имеет тип закрытия (который они назвали лямбда-типом).
@Caleth Это кажется противоречивым. Ответ говорит, что prvalue — это выражение, оценка которого инициализирует объект. И затем этот аргумент по умолчанию оценивается каждый раз, когда вызывается функция.
@Алан, есть одно выражение одного типа. «Тип лямбда-выражения представляет собой уникальный безымянный тип класса без объединения»
@Caleth - однако есть предостережение: лямбда-закрытие будет иметь внутреннюю связь, как если бы оно находилось в анонимном пространстве имен, поэтому объявления в разных единицах перевода будут иметь свою собственную лямбда-выражение.
Этот пример из dcl.fct.default довольно ясно показывает, что цель состоит в том, чтобы точка, в которой определен аргумент по умолчанию, также определяла семантику:
int a = 1;
int f(int);
int g(int x = f(a)); // default argument: f(::a)
void h() {
a = 2;
{
int a = 3;
g(); // g(f(::a))
}
}
В частности, аргумент по умолчанию — это не просто последовательность токенов, которая вставляется в момент вызова функции и затем анализируется.
Следуя этому намерению, лямбда-выражение анализируется в момент определения аргумента по умолчанию, а не в момент вызова функции. Следовательно, существует только один тип лямбды, а не много, и правильный результат — 12
.
Однако в Стандарте это недостаточно четко отражено в отношении использования лямбда-выражений в качестве аргументов по умолчанию.
Итак, вы имеете в виду предложение «Имена в аргументе по умолчанию просматриваются и семантические ограничения проверяются в тот момент, когда появляется аргумент по умолчанию (...)»? Если да, то к части о поиске имени (это означает, что operator()
ищется только один раз) или к части о семантических ограничениях (что это означает)? Примеры хорошие и все такое, но обычно они ненормативны.
@cppbest Я не ссылаюсь на какой-либо нормативный текст, потому что не нашел ничего, что бы говорило конкретно о лямбда-выражениях. Пример просто отражает дух («намерение»): важно, что у нас есть в месте определения аргумента по умолчанию. Затем сайты вызовов просто используют то, что было определено. В случае лямбда-выражения у нас есть только одно определение, и оно используется на сайтах вызова.
Если только это не заголовочный файл, используемый несколькими единицами перевода, в этом случае каждая из них может создавать отдельный тип, который компоновщик может объединять, а может и не объединять.
У меня нет официального доказательства этого, но я подозреваю, что это будет работать так, как вы ожидали, в одной единице перевода, но если вы используете несколько единиц перевода, у вас будет несколько экземпляров. По сути, он будет работать как функция, определенная в заголовке:
static inline int like_lambda()
{
static int x = 0; return ++x;
}
вот понимание cpp.
Вот докажите, что это не работает должным образом при использовании нескольких единиц перевода:
// side.h
#ifndef SIDE_H
#define SIDE_H
int foo(int x = [](){ static int x = 0; return ++x; }());
void side_test();
#endif
// side.cpp
#include "side.h"
#include <iostream>
int foo(int x) { return x; };
void side_test()
{
std::cout << foo() << foo() << '\n';
}
// main.cpp
#include <iostream>
#include "side.h"
int main() {
std::cout << foo() << foo() << '\n';
side_test();
}
Лучшее решение IMO — вернуться к перегрузке функций и упростить код:
// side.h
#ifndef SIDE_H
#define SIDE_H
int foo(int x);
int foo();
void side_test();
#endif
// side.cpp
#include "side.h"
#include <iostream>
int foo()
{
static int x = 0;
return foo(++x);
}
int foo(int x) { return x; };
void side_test()
{
std::cout << foo() << foo() << '\n';
}
это показывает, что лямбда имеет внутреннюю связь, я не думаю, что можно создать лямбду с внешней связью
вот способ «создать» внешнюю связь с лямбдой inline auto fun() { return []{ static int x = 0; return ++x; }(); }
Все это сводится к интерпретации [expr.prim.lambda.closure]/1:
Тип лямбда-выражения (который также является типом объекта замыкания) представляет собой уникальный безымянный тип класса без объединения, называемый типом замыкания, свойства которого описаны ниже.
Что значит «уникальный»?
«Тип лямбда-выражения... уникален...»
Первое слово — «то». Тип. Подразумевается, что лямбда-выражение имеет один тип. Но поскольку оно «уникально», это означает, что любые два лямбда-выражения имеют разные типы.
Слово «лямбда-выражение» выделено курсивом и обозначает грамматический термин. Лямбда, появляющаяся лексически один раз, но вычисляемая более одного раза, представляет собой одно и то же лямбда-выражение при каждой оценке. Таким образом, он имеет один и тот же тип в каждой оценке.
Тот факт, что аргумент по умолчанию оценивается каждый раз при вызове функции, не означает, что программа ведет себя так, как если бы аргумент по умолчанию повторялся дословно в каждом месте вызова. Аргумент по умолчанию — это фрагмент кода, который запускается всякий раз, когда он используется, точно так же, как тело функции.
Однако обратите внимание, что создание экземпляра шаблона удаляет копию каждой грамматической продукции, которая встречается в определении шаблона (хотя для целей поиска имени это не то же самое, что «воспроизведение токенов» в точке создания экземпляра). Другими словами, если у вас есть лямбда-выражение внутри шаблона и вы создаете экземпляр этого шаблона, результирующая специализация имеет свое собственное лямбда-выражение, которое является результатом создания экземпляра выражения из шаблона. Таким образом, каждая специализация получает отдельный тип лямбды, даже если все эти лямбды были определены в одном и том же исходном фрагменте исходного кода.
Также бывают случаи, когда две лямбды, встречающиеся в разных единицах перевода, на самом деле имеют один и тот же тип. Это происходит потому, что существует правило, которое может заставить несколько одинаковых фрагментов исходного кода из разных единиц перевода вести себя так, как если бы в программе была только одна копия. [basic.def.odr]/17
Итак, есть случай (шаблоны), когда одна и та же последовательность токенов может обозначать лямбды разных типов. Противоречит ли это утверждению «Лямбда, появляющаяся лексически один раз (...), является одним и тем же лямбда-выражением при каждой оценке» (в этом случае существует ли какое-либо правило, гласящее, что вышеуказанное возможно только в шаблонах?) или это так? считали, что экземпляры имеют разные токены?
@cppbest Мне казалось, что я уже объяснил это в своем ответе. Да, каждый раз, когда вы создаете экземпляр шаблона, вы получаете копию каждой грамматической продукции, т. е. вы получаете другое лямбда-выражение.
Когда функция foo
определена, внутри аргумента по умолчанию создается лямбда-выражение и устанавливается его замыкание.
Лямбда фиксирует static int x
внутри своего замыкания. Это означает, что переменная x
связана с самой лямбдой, а не с вызовом конкретной функции.
Каждый вызов foo
без аргумента оценивает лямбду, увеличивая статическую переменную внутри замыкания.
Вот почему вы получаете 12
на выходе, поскольку x
увеличивается от 0 до 1 при первом вызове, а затем от 1 до 2 при втором вызове.
Не могли бы вы рассказать подробнее о том, «создается лямбда-выражение внутри аргумента по умолчанию и устанавливается его закрытие»? Меня интересуют ссылки на стандарт
Тип замыкания для лямбда-выражения без лямбда-захвата имеет общедоступную невиртуальную неявную функцию преобразования const в указатель на функцию, имеющую тот же параметр и типы возвращаемых значений, что и оператор вызова функции типа замыкания. Значение, возвращаемое этой функцией преобразования, должно быть адресом функции, которая при вызове имеет тот же эффект, что и вызов оператора вызова функции типа замыкания. timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda#6