Если лямбда объявлена ​​внутри аргумента по умолчанию, отличается ли она для каждого сайта вызова?

#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. выражение оценивается на каждой итерации, но тип лямбда остается прежним.

NathanOliver 01.07.2024 06:28

@HolyBlackCat Лучше обман: Каков тип лямбды, выведенной с помощью «auto» в C++11?

user12002570 01.07.2024 06:30

@NathanOliver, можешь ли ты привести цитату, объясняющую это? Я думал, что лямбда-выражение описывает уникальный тип, а это означает, что даже если оно синтаксически эквивалентно предыдущему вычислению, оно будет другим.

cppbest 01.07.2024 06:31

@user12002570 user12002570 Я не понимаю, как эти ответы объясняют поведение моего вопроса.

cppbest 01.07.2024 06:32

@cppbest Да. Посмотрите эту демонстрацию

user12002570 01.07.2024 06:37

@cppbest Смотрите каждый раз, когда будет создан новый тип класса, как описано в моем ответе ниже. Демо cppinsights

user12002570 01.07.2024 06:40

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

HolyBlackCat 01.07.2024 10:36

Анализ CPP показывает, что он не будет работать должным образом: cppinsights.io/s/7ca32971

Marek R 01.07.2024 11:41

Это CWG 2300 , как объяснено в ответе

user12002570 01.07.2024 20:21

@HolyBlackCat этот имеет отношение к твоему вопросу

cppbest 02.07.2024 07:13

5 хорошо документированных ответов, но, ИМХО, подведение итогов для простых смертных было бы интересно: - правильно ли, в конечном итоге, поведение, наблюдаемое опом? - какой на самом деле применяется набор правил (кажется, разные части стандарта упоминались в разных ответах, и, честно говоря, я теряю нить?

Oersted 02.07.2024 10:06

Вы можете получить 11 результат, преобразуя функцию в шаблон функции: godbolt.org/z/3vhhKGnEe

Fedor 02.07.2024 17:18

@cppbest «Меня интересуют стандартные вопросы или вопросы cwg» Это CWG2300, как описано здесь

Alex 03.07.2024 09:09

@Alex iiuc, решение проблемы уже включено в стандарт, поэтому теперь можно будет ссылаться на точные цитаты для объяснения.

cppbest 03.07.2024 09:20

@cppbest См. часть моего ответа, в которой говорится: «лямбда-выражения, появляющиеся в аргументе D по умолчанию, могут по-прежнему обозначать разные типы в разных единицах перевода». Это должно прояснить ситуацию. Я также дал ссылку на него Basic.def.odr в ответе.

user12002570 03.07.2024 11:50

@user12002570 user12002570 Я думаю, что примечания и примеры не являются нормативными; к тому же эти конкретные цитаты касаются случая множественных ТУ, а у нас только один

cppbest 03.07.2024 12:05

@cppbest Да, примечания не являются нормативными. См. добавленную нормативную часть в ответе, в которой говорится: «Для каждой такой сущности и для самого D поведение такое, как если бы существовала одна сущность с одним определением».

user12002570 03.07.2024 12:13

@cppbest Ключ (нормативная формулировка, которую вы искали) здесь — Basic.def.odr , который объясняет поведение вашей программы и был добавлен в мой ответ. Это делает вывод 12 правильным. По сути, у вас есть невстроенная нешаблонная функция, которая может быть определена только в одном TU. Это означает, что может быть только одно определение самой лямбды. Это вместе со старой частью ответа проясняет ситуацию.

user12002570 09.07.2024 08:03
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
36
20
3 556
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Это CWG 2300 , и поведение программы можно объяснить с помощью Basic.def.odr#15.6 и Basic.def.odr, что означает, что вывод 12 правильный.

Из Basic.def.odr#15.6:

  1. Для любого определяемого элемента 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 01.07.2024 06:42

@cppbest «Это ошибка?...» Выглядит так и детали реализации. Хотя вывод должен соответствовать поведению, которого здесь нет.

user12002570 01.07.2024 06:44

К вашему сведению: gcc, clang, MSVC согласны с тем, что 12 — правильный результат — в прямом эфире — godbolt.org/z/5vYPWxKnM

Richard Critten 01.07.2024 07:55

@cppbest Я добавил надуманный пример, который должен прояснить ситуацию. По сути, обе эти программы (ваша и моя) должны выйти из строя по одной и той же причине.

user12002570 01.07.2024 07:57

@RichardCritten Технически это не означает, что вывод правильный. Например, все компиляторы также принимают эту некорректную программу. Таким образом, согласие на один и тот же вывод не делает программу правильной. Я также видел много других примеров, когда все компиляторы принимают какую-то некорректную программу или выдают неправильный вывод.

user12002570 01.07.2024 08:34

Я не понимаю, как абзац «оценивается каждый раз» что-либо доказывает. auto f() { return []{}; } также каждый раз оценивает лямбду, и это определенно не каждый раз другой тип.

Passer By 01.07.2024 08:41

@PasserBy Насколько я понимаю, это ошибка/недостаток стандарта. И согласно текущей редакции программа должна вывести 11. Прежде чем пытаться проверить ситуацию примерами/аналогиями, вам следует сначала предоставить какое-то стандартное предложение, объясняющее поведение.

user12002570 01.07.2024 08:43

@PasserBy Спросите себя, является ли [](){//...} аргументом по умолчанию. Да, это. Тогда согласно [dcl.fct.default] здесь он будет оцениваться дважды. Теперь спросите себя, является ли тип некоторых заданных лямбда-выражений уникальным. Да, согласно [expr.prim.lambda.closure] это так. Я хочу сказать, что стандарт мог бы прояснить это. То есть есть разница между тем, что должно быть и тем, что есть на данный момент по стандарту.

user12002570 01.07.2024 08:48

Он будет оценен дважды, но это ничего не говорит о его типе, см. пример в предыдущем комментарии.

Passer By 01.07.2024 08:53

@PasserBy, который ничего не говорит о его типе... Чтобы получить тип, необходима оценка. Как я уже сказал, это стандартный недостаток.

user12002570 01.07.2024 08:55

Пришло время подать DR.

Red.Wave 01.07.2024 08:56

@Red.Wave Да, у меня уже есть.

user12002570 01.07.2024 08:56

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

Swift - Friday Pie 01.07.2024 09:36

@Swift-FridayPie Да, именно поэтому я предложил добавить «во время определения (не во время оценки)» в упомянутую проблему cwg, поскольку это сделает это более понятным.

user12002570 01.07.2024 09:41

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

Swift - Friday Pie 01.07.2024 09:46

«должно произойти сбой по той же причине, что и...» Этот пример отличается тем, что вы пишете лямбду дважды.

HolyBlackCat 01.07.2024 10:08

Ошибка отсутствует. Лямбда объявляется один раз, существует только один тип лямбды, он вычисляется много раз.

Gene 01.07.2024 10:28

@Gene Вот почему предложено изменение в проблеме cwg.

user12002570 01.07.2024 10:38

@Gene Вот как работает языковой ответ.

user12002570 01.07.2024 10:46

@user12002570 user12002570 эта проблема была открыта 2 часа назад. Дайте cwg немного времени, чтобы решить проблему как «уже работает».

Caleth 01.07.2024 10:53

@HolyBlackCat Я так и предполагал, да

Caleth 01.07.2024 11:04

iiuc, цитата про шаблоны, но в вопросе нет шаблона

cppbest 03.07.2024 14:50

@cppbest Только первая часть/предложение (которое не выделено) касается шаблонов. Остальное (выделено) относится и к нешаблонным. Вот почему примечание и пример нешаблонов приведены чуть ниже.

user12002570 03.07.2024 17:02

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

Поскольку мы говорим о лямбда-выражении, мы имеем дело с prvalue. Оценка prvalue инициализирует объект, имеющий ровно один тип. [expr.prim.lambda.closure] [dcl.fnc.default] [basic.lval] [intro.object]

Лямбда-выражение — это prvalue, объект результата которого называется объектом замыкания.

Тип лямбда-выражения (который также является типом объекта замыкания) представляет собой уникальный безымянный тип класса без объединения, называемый типом замыкания, свойства которого описаны ниже.

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

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

Свойства объекта определяются при его создании. Объект может иметь имя. Объект имеет продолжительность хранения, которая влияет на его срок службы. У объекта есть тип.

Выражение остается одного и того же типа на протяжении всего выполнения [defns.static.type]

статический тип

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

12 — правильный результат для вашего примера.

Вопрос лингвистический. Весь смысл в том, чтобы найти стандартную ссылку/предложение. Часто все три компилятора принимают неправильно сформированную или неправильную программу.

user12002570 01.07.2024 10:25

@user12002570 user12002570 на самом деле очень сложно найти стандартную цитату о том, что «выражение остается одного и того же типа на протяжении всего выполнения»

Caleth 01.07.2024 10:28

Это задача.

user12002570 01.07.2024 10:29

Цитаты не подтверждают ваш вывод. Вы только что добавили цитирование, чтобы этот ответ выглядел как ответ лингвиста.

user12002570 01.07.2024 10:42

Я согласен, что это правильный ответ, но если вы собираетесь цитировать те же цитаты, что и в другом ответе, я предлагаю объяснить, почему ваша интерпретация верна, а их — нет.

HolyBlackCat 01.07.2024 10:42

Как одни и те же цитаты могут означать разные/противоположные вещи. Стандарт нуждается в некоторой модификации, чтобы прояснить это, как указано в представленном выпуске cwg.

user12002570 01.07.2024 10:43

Отредактированный ответ противоречит выводу этого ответа?

user12002570 01.07.2024 10:45

@user12002570 user12002570 нет, вы просто совершенно не понимаете определение и оценку.

Caleth 01.07.2024 10:47

@Калет Нет, я точно знаю, что они имеют в виду. Вот почему предложенное изменение в выпуске cwg. Отредактированный ответ по-прежнему ничего не объясняет по заданному вопросу. ОП уже знает, что объект имеет тип, а лямбда имеет тип закрытия (который они назвали лямбда-типом).

user12002570 01.07.2024 10:47

@Caleth Это кажется противоречивым. Ответ говорит, что prvalue — это выражение, оценка которого инициализирует объект. И затем этот аргумент по умолчанию оценивается каждый раз, когда вызывается функция.

Alan 01.07.2024 10:57

@Алан, есть одно выражение одного типа. «Тип лямбда-выражения представляет собой уникальный безымянный тип класса без объединения»

Caleth 01.07.2024 11:03

@Caleth - однако есть предостережение: лямбда-закрытие будет иметь внутреннюю связь, как если бы оно находилось в анонимном пространстве имен, поэтому объявления в разных единицах перевода будут иметь свою собственную лямбда-выражение.

Gene 01.07.2024 19:28

Этот пример из 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 02.07.2024 06:27

@cppbest Я не ссылаюсь на какой-либо нормативный текст, потому что не нашел ничего, что бы говорило конкретно о лямбда-выражениях. Пример просто отражает дух («намерение»): важно, что у нас есть в месте определения аргумента по умолчанию. Затем сайты вызовов просто используют то, что было определено. В случае лямбда-выражения у нас есть только одно определение, и оно используется на сайтах вызова.

j6t 02.07.2024 07:46

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

Miral 02.07.2024 08:07

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

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

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

Gene 01.07.2024 19:23

вот способ «создать» внешнюю связь с лямбдой inline auto fun() { return []{ static int x = 0; return ++x; }(); }

Gene 01.07.2024 19:35
Ответ принят как подходящий

Все это сводится к интерпретации [expr.prim.lambda.closure]/1:

Тип лямбда-выражения (который также является типом объекта замыкания) представляет собой уникальный безымянный тип класса без объединения, называемый типом замыкания, свойства которого описаны ниже.

Что значит «уникальный»?

«Тип лямбда-выражения... уникален...»

Первое слово — «то». Тип. Подразумевается, что лямбда-выражение имеет один тип. Но поскольку оно «уникально», это означает, что любые два лямбда-выражения имеют разные типы.

Слово «лямбда-выражение» выделено курсивом и обозначает грамматический термин. Лямбда, появляющаяся лексически один раз, но вычисляемая более одного раза, представляет собой одно и то же лямбда-выражение при каждой оценке. Таким образом, он имеет один и тот же тип в каждой оценке.

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

Однако обратите внимание, что создание экземпляра шаблона удаляет копию каждой грамматической продукции, которая встречается в определении шаблона (хотя для целей поиска имени это не то же самое, что «воспроизведение токенов» в точке создания экземпляра). Другими словами, если у вас есть лямбда-выражение внутри шаблона и вы создаете экземпляр этого шаблона, результирующая специализация имеет свое собственное лямбда-выражение, которое является результатом создания экземпляра выражения из шаблона. Таким образом, каждая специализация получает отдельный тип лямбды, даже если все эти лямбды были определены в одном и том же исходном фрагменте исходного кода.

Также бывают случаи, когда две лямбды, встречающиеся в разных единицах перевода, на самом деле имеют один и тот же тип. Это происходит потому, что существует правило, которое может заставить несколько одинаковых фрагментов исходного кода из разных единиц перевода вести себя так, как если бы в программе была только одна копия. [basic.def.odr]/17

Итак, есть случай (шаблоны), когда одна и та же последовательность токенов может обозначать лямбды разных типов. Противоречит ли это утверждению «Лямбда, появляющаяся лексически один раз (...), является одним и тем же лямбда-выражением при каждой оценке» (в этом случае существует ли какое-либо правило, гласящее, что вышеуказанное возможно только в шаблонах?) или это так? считали, что экземпляры имеют разные токены?

cppbest 02.07.2024 06:50

@cppbest Мне казалось, что я уже объяснил это в своем ответе. Да, каждый раз, когда вы создаете экземпляр шаблона, вы получаете копию каждой грамматической продукции, т. е. вы получаете другое лямбда-выражение.

Brian Bi 03.07.2024 01:29

Когда функция foo определена, внутри аргумента по умолчанию создается лямбда-выражение и устанавливается его замыкание. Лямбда фиксирует static int x внутри своего замыкания. Это означает, что переменная x связана с самой лямбдой, а не с вызовом конкретной функции. Каждый вызов foo без аргумента оценивает лямбду, увеличивая статическую переменную внутри замыкания. Вот почему вы получаете 12 на выходе, поскольку x увеличивается от 0 до 1 при первом вызове, а затем от 1 до 2 при втором вызове.

Не могли бы вы рассказать подробнее о том, «создается лямбда-выражение внутри аргумента по умолчанию и устанавливается его закрытие»? Меня интересуют ссылки на стандарт

cppbest 03.07.2024 14:52

Тип замыкания для лямбда-выражения без лямбда-захвата имеет общедоступную невиртуальную неявную функцию преобразования const в указатель на функцию, имеющую тот же параметр и типы возвращаемых значений, что и оператор вызова функции типа замыкания. Значение, возвращаемое этой функцией преобразования, должно быть адресом функции, которая при вызове имеет тот же эффект, что и вызов оператора вызова функции типа замыкания. timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda#6

Vahagn Avagyan 03.07.2024 20:21

Другие вопросы по теме