Рассмотрим эту довольно бесполезную программу:
#include <iostream>
int main(int argc, char* argv[]) {
int a = 5;
auto it = [&](auto self) {
return [&](auto b) {
std::cout << (a + b) << std::endl;
return self(self);
};
};
it(it)(4)(6)(42)(77)(999);
}
По сути, мы пытаемся сделать лямбду, которая возвращает сама себя.
- MSVC компилирует программу, и она запускается
- gcc компилирует программу, и она перестает работать
- clang отклоняет программу с сообщением:
error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined
Какой компилятор правильный? Есть ли нарушение статического ограничения, UB или нет?
Обновить это небольшое изменение принято clang:
auto it = [&](auto& self, auto b) {
std::cout << (a + b) << std::endl;
return [&](auto p) { return self(self,p); };
};
it(it,4)(6)(42)(77)(999);
Обновление 2 : я понимаю, как написать функтор, который возвращает себя, или как использовать комбинатор Y для этого. Это больше вопрос языкового юриста.
Обновление 3 : вопрос не в том, законно ли лямбда-выражение возвращать себя в целом, а в законности этого конкретного способа сделать это.
Связанный вопрос: C ++ lambda возвращает себя .
auto& self
что устраняет проблему с висячими ссылками.Ответы:
Программа плохо сформирована (clang прав) согласно [dcl.spec.auto] / 9 :
По сути, вывод типа возвращаемого значения внутренней лямбды зависит от него самого (названная здесь сущность является оператором вызова), поэтому вы должны явно указать тип возвращаемого значения. В данном конкретном случае это невозможно, потому что вам нужен тип внутренней лямбды, но вы не можете назвать его. Но есть и другие случаи, когда попытка принудительно использовать рекурсивные лямбды вроде этого может сработать.
Даже без этого у вас есть свисающая ссылка .
Позвольте мне уточнить некоторые детали после обсуждения с кем-то более умным (например, TC). Существует важное различие между исходным кодом (немного сокращенным) и предлагаемой новой версией (также сокращенным):
И это то, что внутреннее выражение
self(self)
не зависит отf1
, ноself(self, p)
зависит отf2
. Когда выражения не зависят друг от друга , их можно использовать ... с готовностью ( [temp.res] / 8 , например, какstatic_assert(false)
является серьезной ошибкой, независимо от того, создан ли шаблон, в котором она находится, или нет).Ибо
f1
компилятор (например, clang) может с нетерпением попытаться создать его экземпляр. Вы узнаете выведенный тип внешней лямбды, как только дойдете до этого;
в точке#2
выше (это тип внутренней лямбды), но мы пытаемся использовать его раньше (думайте об этом как в точке#1
) - мы пытаемся чтобы использовать его, пока мы еще разбираем внутреннюю лямбду, прежде чем мы узнаем, что это за тип на самом деле. Это противоречит dcl.spec.auto/9.Однако
f2
мы не можем пытаться создать экземпляр с нетерпением, потому что это зависит. Мы можем создать экземпляр только в момент использования, и тогда мы все знаем.Чтобы действительно сделать что-то подобное, вам понадобится y-комбинатор . Реализация из бумаги:
А вы хотите:
источник
Изменить : похоже, есть некоторые разногласия по поводу того, является ли эта конструкция строго допустимой в соответствии со спецификацией C ++. Преобладает мнение, что это неверно. См. Другие ответы для более подробного обсуждения. Остальная часть этого ответа применима, если конструкция действительна; измененный код ниже работает с MSVC ++ и gcc, а OP опубликовал дополнительный измененный код, который также работает с clang.
Это неопределенное поведение, потому что внутренняя лямбда захватывает параметр
self
по ссылке, ноself
выходит за пределы области действия послеreturn
строки 7. Таким образом, когда возвращенная лямбда выполняется позже, она обращается к ссылке на переменную, которая вышла из области видимости.Запуск программы с помощью
valgrind
иллюстрирует это:Вместо этого вы можете изменить внешнюю лямбду так, чтобы она брала себя по ссылке, а не по значению, тем самым избегая кучи ненужных копий, а также решая проблему:
Это работает:
источник
self
ссылку?self
ссылку, эта проблема исчезнет , но Clang все равно отвергает ее по другой причинеself
это зафиксировано по ссылке!TL; DR;
лязг правильный.
Похоже, что раздел стандарта, который делает это некорректным, это [dcl.spec.auto] p9 :
Оригинальная работа через
Если мы посмотрим на предложение A Proposal to Add Y Combinator to Standard Library, оно дает рабочее решение:
и он явно говорит, что ваш пример невозможен:
и он ссылается на дискуссию, в которой Ричард Смит намекает на ошибку, которую выдает вам clang :
Барри указал мне на последующее предложение Рекурсивные лямбды, в котором объясняется, почему это невозможно, и
dcl.spec.auto#9
обходится ограничение, а также показаны методы достижения этого сегодня без этого:источник
self
Не похоже на такой объект.Кажется, лязг - это правильно. Рассмотрим упрощенный пример:
Пройдемся как компилятор (немного):
it
-Lambda1
с оператором вызова шаблона.it(it);
запускает создание оператора вызоваauto
, поэтому мы должны вывести его.Lambda1
.self(self)
self(self)
это именно то, с чего мы начали!Таким образом, тип не может быть выведен.
источник
Lambda1::operator()
- это простоLambda2
. Затем в этом внутреннем лямбда-выражении также известен тип возвращаемого значенияself(self)
- вызов . Возможно, формальные правила мешают сделать этот тривиальный вывод, но представленная здесь логика - нет. Логика здесь сводится к утверждению. Если формальные правила все же стоят на пути, то это недостаток формальных правил.Lambda1::operator()
Lambda2
Что ж, ваш код не работает. Но это делает:
Код теста:
Ваш код является одновременно UB и неправильно сформированным, диагностика не требуется. Что забавно; но оба могут быть исправлены независимо.
Во-первых, УБ:
это UB, потому что внешний принимает
self
значение по значению, затем внутренний захватываетсяself
по ссылке, а затем возвращается к нему послеouter
завершения выполнения. Так что segfaulting определенно нормально.Исправление:
Код остается некорректным. Чтобы увидеть это, мы можем расширить лямбды:
это создает
__outer_lambda__::operator()<__outer_lambda__>
:Итак, теперь нам нужно определить тип возвращаемого значения
__outer_lambda__::operator()
.Мы проходим его строка за строкой. Сначала мы создаем
__inner_lambda__
тип:Теперь посмотрите туда - его возвращаемый тип -
self(self)
или__outer_lambda__(__outer_lambda__ const&)
. Но мы находимся в процессе определения типа возвращаемого значения__outer_lambda__::operator()(__outer_lambda__)
.Тебе нельзя этого делать.
Хотя на самом деле тип возвращаемого значения
__outer_lambda__::operator()(__outer_lambda__)
не зависит от типа возвращаемого значения__inner_lambda__::operator()(int)
, C ++ не заботится при выводе типов возвращаемых значений ; он просто проверяет код построчно.И
self(self)
используется до того, как мы его вывели. Плохо сформированная программа.Мы можем исправить это, спрятав на
self(self)
потом:и теперь код правильный и компилируется. Но я думаю, что это небольшая хитрость; просто используйте ycombinator.
источник
operator()
, как правило, не может быть выведен до его создания (путем вызова с некоторым аргументом некоторого типа). Так что ручная машинная перезапись кода на основе шаблонов работает хорошо.Достаточно легко переписать код в терминах классов, которые компилятор должен или, скорее, должен генерировать для лямбда-выражений.
Когда это будет сделано, станет ясно, что основная проблема - это просто висящая ссылка и что компилятор, который не принимает код, сталкивается с некоторыми проблемами в области лямбда.
Переписывание показывает, что циклических зависимостей нет.
Полностью шаблонная версия, отражающая способ, которым внутренняя лямбда в исходном коде захватывает элемент шаблонного типа:
Я полагаю, что именно этот шаблон во внутреннем механизме, что формальные правила призваны запретить. Если они все же запретят исходную конструкцию.
источник
template< class > class Inner;
шаблонoperator()
... создан? Что ж, неправильное слово. Письменное? ... во времяOuter::operator()<Outer>
до того, как будет выведен тип возврата внешнего оператора. ИInner<Outer>::operator()
имеет призыв кOuter::operator()<Outer>
себе. А это запрещено. Теперь, большинство компиляторов не замечаютself(self)
, потому что они ждут , чтобы вывести тип возвратаOuter::Inner<Outer>::operator()<int>
, когдаint
передается в. Sensible. Но он упускает из виду плохо сформированный код.Innner<T>::operator()<U>
будет создан. Ведь возвращаемый тип может зависеть от типаU
здесь. Нет, но в целом.