Лямбда возвращается сама: это законно?

Рассмотрим эту довольно бесполезную программу:

#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++ возвращает себя.

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

bipll 05.09.2018 21:48

Когда я запустите этот пример с msvc, иногда он проходит, но обычно заканчивается ошибкой seg.

Marek R 05.09.2018 22:00

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

Shafik Yaghmour 05.09.2018 22:41

@ShafikYaghmour Спасибо, добавил тег

n. 1.8e9-where's-my-share m. 05.09.2018 23:12

@ArneVogel да, обновленный использует auto& self, который устраняет проблему с висячими ссылками.

n. 1.8e9-where's-my-share m. 06.09.2018 08:29

@TheGreatDuck лямбда-выражения C++ на самом деле не являются теоретическими лямбда-выражениями. C++ имеет встроенный рекурсивный типы, который исходное простое типизированное лямбда-исчисление не может выразить, поэтому он может иметь вещи, изоморфные a: a-> a и другим невозможным конструкциям.

n. 1.8e9-where's-my-share m. 07.09.2018 08:05

Смутно связанный: stackoverflow.com/q/8595061/560648

Lightness Races in Orbit 07.09.2018 12:40
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
127
7
9 884
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Редактировать: Кажется, есть некоторые разногласия по поводу того, является ли эта конструкция строго допустимой в соответствии со спецификацией C++. Преобладает мнение, что это неверно. См. Другие ответы для более подробного обсуждения. Остальная часть этого ответа применяет если, конструкция действительна; измененный код ниже работает с MSVC++ и gcc, а OP опубликовал дополнительный измененный код, который также работает с clang.

Это неопределенное поведение, потому что внутренняя лямбда захватывает параметр self по ссылке, но self выходит за пределы области действия после return в строке 7. Таким образом, когда возвращенная лямбда выполняется позже, она обращается к ссылке на переменную, которая вышла из строя. объема.

#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); // <-- using reference to 'self'
      };
  };
  it(it)(4)(6)(42)(77)(999); // <-- 'self' is now out of scope
}

Запуск программы с valgrind иллюстрирует это:

==5485== Memcheck, a memory error detector
==5485== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5485== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5485== Command: ./test
==5485== 
9
==5485== Use of uninitialised value of size 8
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485== 
==5485== Invalid read of size 4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  Address 0x4fefffdc4 is not stack'd, malloc'd or (recently) free'd
==5485== 
==5485== 
==5485== Process terminating with default action of signal 11 (SIGSEGV)
==5485==  Access not within mapped region at address 0x4FEFFFDC4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  If you believe this happened as a result of a stack
==5485==  overflow in your program's main thread (unlikely but
==5485==  possible), you can try to increase the size of the
==5485==  main thread stack using the --main-stacksize= flag.
==5485==  The main thread stack size used in this run was 8388608.

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

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto& self) { // <-- self is now a reference
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self);
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

Это работает:

==5492== Memcheck, a memory error detector
==5492== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5492== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5492== Command: ./test
==5492== 
9
11
47
82
1004

Я не знаком с общими лямбдами, но не могли бы вы сделать self ссылкой?

François Andrieux 05.09.2018 21:53

@ FrançoisAndrieux Да, если вы сделаете self ссылкой, эта проблема уходит, но Clang все равно отвергает это по другой причине

Justin 05.09.2018 21:54

@ FrançoisAndrieux Действительно, и я добавил это к ответу, спасибо!

TypeIA 05.09.2018 21:57

Проблема с этим подходом в том, что он не устраняет возможные ошибки компилятора. Так что, возможно, это должно сработать, но реализация не работает.

Shafik Yaghmour 05.09.2018 22:25

Спасибо, я смотрел на это часами и не заметил, что self захвачен по ссылке!

n. 1.8e9-where's-my-share m. 05.09.2018 23:23

Кажется, лязг - это правильно. Рассмотрим упрощенный пример:

auto it = [](auto& self) {
    return [&self]() {
      return self(self);
    };
};
it(it);

Давайте пройдемся по нему как с компилятором (немного):

  • Тип it - Lambda1 с оператором вызова шаблона.
  • it(it); запускает создание оператора вызова
  • Тип возврата оператора вызова шаблона - auto, поэтому мы должны его вывести.
  • Мы возвращаем лямбда, захватывающую первый параметр типа Lambda1.
  • Эта лямбда также имеет оператор вызова, который возвращает тип вызова self(self).
  • Примечание: self(self) - это именно то, с чего мы начали!

Таким образом, тип не может быть выведен.

Тип возврата Lambda1::operator() - просто Lambda2. Затем внутри этого внутреннего лямбда-выражения тип возврата self(self), вызов Lambda1::operator(), также известен как Lambda2. Возможно, формальные правила мешают сделать этот тривиальный вывод, но представленная здесь логика - нет. Логика здесь сводится к утверждению. Если формальные правила действительно мешают, то это недостаток формальных правил.

Cheers and hth. - Alf 05.09.2018 22:30

@ Cheersandhth.-Alf Я согласен с тем, что тип возвращаемого значения - Lambda2, но вы знаете, что у вас не может быть невыявленного оператора вызова только потому, что это то, что вы предлагаете: отложить вывод типа возвращаемого значения оператора вызова Lambda2. Но вы не можете изменить правила для этого, так как это довольно фундаментально.

Rakete1111 05.09.2018 22:41
Ответ принят как подходящий

Программа плохо сформирована (clang правый) на [dcl.spec.auto] / 9:

If the name of an entity with an undeduced placeholder type appears in an expression, the program is ill-formed. Once a non-discarded return statement has been seen in a function, however, the return type deduced from that statement can be used in the rest of the function, including in other return statements.

По сути, вывод типа возвращаемого значения внутренней лямбды зависит от него самого (названная здесь сущность является оператором вызова), поэтому вы должны явно указать тип возвращаемого значения. В данном конкретном случае это невозможно, потому что вам нужен тип внутренней лямбды, но вы не можете назвать его. Но есть и другие случаи, когда попытка принудительно использовать такие рекурсивные лямбды может сработать.

Даже без этого у вас есть висячая ссылка.


Позвольте мне уточнить некоторые детали после обсуждения с кем-то более умным (например, T.C.). Существует важное различие между исходным кодом (слегка сокращенным) и предлагаемой новой версией (также сокращенным):

auto f1 = [&](auto& self) {
  return [&](auto) { return self(self); } /* #1 */ ; /* #2 */
};
f1(f1)(0);

auto f2 = [&](auto& self, auto) {
  return [&](auto p) { return self(self,p); };
};
f2(f2, 0);

И это то, что внутренняя экспрессия self(self) не зависит от f1, но self(self, p) зависит от f2. Когда выражения независимы, их можно использовать ... с готовностью ([temp.res] / 8, например, как static_assert(false) является серьезной ошибкой, независимо от того, создан ли шаблон, в котором он находится, или нет).

Для f1 компилятор (например, clang) может попытаться быстро создать экземпляр. Вы знаете выведенный тип внешней лямбды, как только вы дойдете до этого ; в точке #2 выше (это тип внутренней лямбды), но мы пытаемся использовать его раньше (подумайте об этом как в точке #1) - мы пытаемся использовать его, пока мы все еще анализируем внутреннюю лямбду, прежде чем мы узнаем, что это за тип на самом деле. Это противоречит dcl.spec.auto/9.

Однако для f2 мы не можем пытаться инстанцировать с нетерпением, потому что это зависит. Мы можем создать экземпляр только в момент использования, и к этому моменту мы все знаем.


Чтобы действительно сделать что-то подобное, вам понадобится y-комбинатор. Реализация из бумаги:

template<class Fun>
class y_combinator_result {
    Fun fun_;
public:
    template<class T>
    explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}

    template<class ...Args>
    decltype(auto) operator()(Args &&...args) {
        return fun_(std::ref(*this), std::forward<Args>(args)...);
    }
};

template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
    return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

А вы хотите:

auto it = y_combinator([&](auto self, auto b){
    std::cout << (a + b) << std::endl;
    return self;
});

Как бы вы явно указали тип возвращаемого значения? Я не могу понять.

Rakete1111 05.09.2018 22:41

@ Rakete1111 Какой? В оригинале нет.

Barry 05.09.2018 22:42

Ох, хорошо. Я не родной, но «поэтому вы должны явно указать тип возвращаемого значения», похоже, подразумевает, что есть способ, поэтому я и спрашивал :)

Rakete1111 05.09.2018 22:46

@ Rakete1111 О да, это вводит в заблуждение. Я имел в виду случай, когда, возможно, вы знаете, что такое возвращаемый тип (например, вы пишете рекурсивные фибоначчи, поэтому вы знаете, что это int, хотя это зависит от self)

Barry 05.09.2018 22:47

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

n. 1.8e9-where's-my-share m. 05.09.2018 23:17

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

Barry 05.09.2018 23:25

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

n. 1.8e9-where's-my-share m. 05.09.2018 23:28

@ n.m. Помогает ли это дополнение?

Barry 06.09.2018 00:54

Да, ключевым моментом здесь является знание того, какие имена являются зависимыми.

n. 1.8e9-where's-my-share m. 06.09.2018 05:03

@PedroA stackoverflow.com/users/2756719/t-c - участник C++. Он также либо нет искусственный интеллект, либо достаточно изобретательный, чтобы убедить человека, который также хорошо разбирается в C++, посетить недавнюю мини-встречу LWG в Чикаго.

Casey 07.09.2018 05:07

@Casey Или, может быть, человек просто повторяет то, что ему сказал ИИ ... никогда не знаешь;)

T.C. 08.09.2018 06:00

TL; DR;

лязг правильный.

Похоже, что раздел стандарта, который делает это некорректным, - это [dcl.spec.auto] стр. 9:

If the name of an entity with an undeduced placeholder type appears in an expression, the program is ill-formed. Once a non-discarded return statement has been seen in a function, however, the return type deduced from that statement can be used in the rest of the function, including in other return statements. [ Example:

auto n = n; // error, n’s initializer refers to n
auto f();
void g() { &f; } // error, f’s return type is unknown

auto sum(int i) {
  if (i == 1)
    return i; // sum’s return type is int
  else
    return sum(i-1)+i; // OK, sum’s return type has been deduced
}

—end example ]

Оригинальная работа через

Если мы посмотрим на предложение Предложение добавить Y Combinator в стандартную библиотеку, оно дает рабочее решение:

template<class Fun>
class y_combinator_result {
    Fun fun_;
public:
    template<class T>
    explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}

    template<class ...Args>
    decltype(auto) operator()(Args &&...args) {
        return fun_(std::ref(*this), std::forward<Args>(args)...);
    }
};

template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
    return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

и он явно говорит, что ваш пример невозможен:

C++11/14 lambdas do not encourage recursion: there is no way to reference the lambda object from the body of the lambda function.

и он ссылается на Обсуждение, в котором Ричард Смит намекает на ошибку, которую дает вам лязг:

I think this would be better as a first-class language feature. I ran out of time for the pre-Kona meeting, but I was intending on writing a paper to allow giving a lambda a name (scoped to its own body):

auto x = []fib(int a) { return a > 1 ? fib(a - 1) + fib(a - 2) : a; };

Here, 'fib' is the equivalent of the lambda's *this (with some annoying special rules to allow this to work despite the lambda's closure type being incomplete).

Барри указал мне на последующее предложение Рекурсивные лямбды, в котором объясняется, почему это невозможно, и обходится ограничение dcl.spec.auto#9, а также показаны методы достижения этого сегодня без него:

Lambdas are a useful tool for local code refactoring. However, we sometimes want to use the lambda from within itself, either to permit direct recursion or to allow the closure to be registered as a continuation. This is surprisingly difficult to accomplish well in current C++.

Example:

  void read(Socket sock, OutputBuffer buff) {
  sock.readsome([&] (Data data) {
  buff.append(data);
  sock.readsome(/*current lambda*/);
}).get();

}

One natural attempt to reference a lambda from itself is to store it in a variable and capture that variable by reference:

 auto on_read = [&] (Data data) {
  buff.append(data);
  sock.readsome(on_read);
};

However, this is not possible due to a semantic circularity: the type of the auto variable is not deduced until after the lambda-expression is processed, which means the lambda-expression cannot reference the variable.

Another natural approach is to use a std::function:

 std::function on_read = [&] (Data data) {
  buff.append(data);
  sock.readsome(on_read);
};

This approach compiles, but typically introduces an abstraction penalty: the std::function may incur a memory allocation and the invocation of the lambda will typically require an indirect call.

For a zero-overhead solution, there is often no better approach than defining a local class type explicitly.

@ Cheersandhth.-Alf Я нашел стандартную цитату после прочтения статьи, поэтому она не актуальна, поскольку стандартная цитата дает понять, почему ни один из подходов не работает.

Shafik Yaghmour 05.09.2018 22:46

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

n. 1.8e9-where's-my-share m. 05.09.2018 23:18

@ n.m. Помимо возможных формулировок, примеры, кажется, имеют смысл с формулировкой, и я считаю, что примеры ясно демонстрируют проблему. Я не думаю, что могу добавить что-то еще, чтобы помочь.

Shafik Yaghmour 05.09.2018 23:32

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

Когда это будет сделано, станет ясно, что основная проблема - это просто висящая ссылка и что компилятор, который не принимает код, сталкивается с некоторыми проблемами в области лямбда.

Переписывание показывает, что циклических зависимостей нет.

#include <iostream>

struct Outer
{
    int& a;

    // Actually a templated argument, but always called with `Outer`.
    template< class Arg >
    auto operator()( Arg& self ) const
        //-> Inner
    {
        return Inner( a, self );    //! Original code has dangling ref here.
    }

    struct Inner
    {
        int& a;
        Outer& self;

        // Actually a templated argument, but always called with `int`.
        template< class Arg >
        auto operator()( Arg b ) const
            //-> Inner
        {
            std::cout << (a + b) << std::endl;
            return self( self );
        }

        Inner( int& an_a, Outer& a_self ): a( an_a ), self( a_self ) {}
    };

    Outer( int& ref ): a( ref ) {}
};

int main() {

  int a = 5;

  auto&& it = Outer( a );
  it(it)(4)(6)(42)(77)(999);
}

Полностью шаблонная версия, отражающая способ, которым внутренняя лямбда в исходном коде захватывает элемент шаблонного типа:

#include <iostream>

struct Outer
{
    int& a;

    template< class > class Inner;

    // Actually a templated argument, but always called with `Outer`.
    template< class Arg >
    auto operator()( Arg& self ) const
        //-> Inner
    {
        return Inner<Arg>( a, self );    //! Original code has dangling ref here.
    }

    template< class Self >
    struct Inner
    {
        int& a;
        Self& self;

        // Actually a templated argument, but always called with `int`.
        template< class Arg >
        auto operator()( Arg b ) const
            //-> Inner
        {
            std::cout << (a + b) << std::endl;
            return self( self );
        }

        Inner( int& an_a, Self& a_self ): a( an_a ), self( a_self ) {}
    };

    Outer( int& ref ): a( ref ) {}
};

int main() {

  int a = 5;

  auto&& it = Outer( a );
  it(it)(4)(6)(42)(77)(999);
}

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

Видите ли, проблема в том, что шаблон template< class > class Inner;operator() ... создан? Что ж, неправильное слово. Написано? ... во время Outer::operator()<Outer> до того, как будет выведен тип возврата внешнего оператора. А Inner<Outer>::operator() обращается к самому Outer::operator()<Outer>. А это не разрешено. Теперь большинство компиляторов не уведомлениеself(self), потому что они ждут, чтобы определить тип возврата Outer::Inner<Outer>::operator()<int>, когда int передается. Разумно. Но он упускает из виду плохо сформированный код.

Yakk - Adam Nevraumont 05.09.2018 23:18

Что ж, я думаю, что они должен ждут, чтобы определить тип возвращаемого значения шаблона функции, пока этот шаблон функции, Innner<T>::operator()<U>, не будет создан. В конце концов, здесь тип возвращаемого значения может зависеть от U. Нет, но в целом.

Cheers and hth. - Alf 05.09.2018 23:29

Конечно; но любое выражение, тип которого определяется неполным выводом типа возвращаемого значения, остается незаконным. Просто некоторые компиляторы ленивы и не проверяют их позже, когда все работает.

Yakk - Adam Nevraumont 06.09.2018 00:34

Что ж, ваш код не работает. Но это действительно так:

template<class F>
struct ycombinator {
  F f;
  template<class...Args>
  auto operator()(Args&&...args){
    return f(f, std::forward<Args>(args)...);
  }
};
template<class F>
ycombinator(F) -> ycombinator<F>;

Код теста:

ycombinator bob = {[x=0](auto&& self)mutable{
  std::cout << ++x << "\n";
  ycombinator ret = {self};
  return ret;
}};

bob()()(); // prints 1 2 3

Ваш код имеет форму UB и имеет неверный формат, диагностика не требуется. Что забавно; но оба могут быть исправлены независимо.

Во-первых, УБ:

auto it = [&](auto self) { // outer
  return [&](auto b) { // inner
    std::cout << (a + b) << std::endl;
    return self(self);
  };
};
it(it)(4)(5)(6);

это UB, потому что внешний принимает self по значению, затем внутренний захватывает self по ссылке, а затем возвращает его после завершения работы outer. Так что segfaulting определенно нормально.

Исправление:

[&](auto self) {
  return [self,&a](auto b) {
    std::cout << (a + b) << std::endl;
    return self(self);
  };
};

Код остается некорректным. Чтобы увидеть это, мы можем расширить лямбды:

struct __outer_lambda__ {
  template<class T>
  auto operator()(T self) const {
    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      T self;
    };
    return __inner_lambda__{a, self};
  }
  int& a;
};
__outer_lambda__ it{a};
it(it);

это создает экземпляр __outer_lambda__::operator()<__outer_lambda__>:

  template<>
  auto __outer_lambda__::operator()(__outer_lambda__ self) const {
    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      __outer_lambda__ self;
    };
    return __inner_lambda__{a, self};
  }
  int& a;
};

Итак, теперь нам нужно определить тип возвращаемого значения __outer_lambda__::operator().

Мы проходим его строка за строкой. Сначала мы создаем тип __inner_lambda__:

    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      __outer_lambda__ self;
    };

Теперь посмотрите туда - его тип возврата - 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) на потом:

template<class A, class B>
struct second_type_helper { using result=B; };

template<class A, class B>
using second_type = typename second_type_helper<A,B>::result;

int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [self,&a](auto b) {
        std::cout << (a + b) << std::endl;
        return self(second_type<decltype(b), decltype(self)&>(self) );
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

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

Возможно (IDK) это описание соответствует формальным правилам лямбда-выражений. Но с точки зрения перезаписи шаблона тип возвращаемого значения внутреннего лямбда-шаблона operator(), как правило, не может быть выведен до его создания (путем вызова с некоторым аргументом некоторого типа). Так что ручная машинная перезапись кода на основе шаблона прекрасно работает.

Cheers and hth. - Alf 05.09.2018 23:48

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

Yakk - Adam Nevraumont 06.09.2018 00:36

Класс, определенный в шаблонной функции, эквивалентен шаблонному классу вне этой функции. Определение его вне функции необходимо для демонстрационного кода, когда он имеет шаблонную функцию-член, потому что правила C++ не разрешают шаблон-член в локальном определяемом пользователем классе. Это формальное ограничение не распространяется на все, что компилятор генерирует сам.

Cheers and hth. - Alf 06.09.2018 00:55

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