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


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

#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 компилирует программу, и она segfaults
  • 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++ возвращается сам.

6 116

6 ответов:

программа плохо сформирована (clang is right) per [dcl.спекуляция.auto] / 9:

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

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

даже без этого, у вас есть подвешенные.


позвольте мне подробнее, после обсуждения с кем-то гораздо умнее (т. е. 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) - мы пытаемся использовать его, пока мы все еще анализируем внутреннюю лямбду, прежде чем мы узнаем, что это тип на самом деле. Что идет вразрез с РСН.спекуляция.авто/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;
});

Edit:кажется, есть некоторые разногласия по поводу того, является ли эта конструкция строго допустимой для спецификации 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

TL; DR;

Clang является правильным.

похоже, что раздел стандарта, который делает это плохо сформированным,[dcl.спекуляция.авто]Р9:

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

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
}

-конец примера ]

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

если мы посмотрим на предложение предложение добавить 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 лямбды не поощряют рекурсии: нет никакого способа, чтобы ссылаться на лямбда-объект из тела лямбда-функции.

и он ссылается на дикция, в которой Ричард Смит намекает на ошибку, которую дает вам clang:

Я думаю, что это было бы лучше в качестве первоклассной языковой функции. У меня не было времени для встречи перед Кона, но я намеревался написать статью, чтобы позволить дать лямбде имя (охватываемое ее собственным телом):

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

здесь, привирать-это эквивалент лямбды *this (с некоторыми раздражающими специальными правилами, позволяющими это работать, несмотря на то, что тип закрытия лямбды является неполным).

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

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

пример:

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

}

одна естественная попытка ссылаться на лямбду из себя-это сохранить ее в переменной и захватить эту переменную по ссылке:

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

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

другой естественный подход заключается в использовании std:: function:

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

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

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

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

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

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

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

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

Ну, ваш код не работает. Но это:

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, так и плохо сформированный не требуется диагностика. Что забавно, но оба могут быть исправлены самостоятельно.

во-первых, 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.

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

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

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

#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);
}

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