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


125

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

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


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

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

2
@ShafikYaghmour Спасибо, добавили тег
n. 'местоимения' м.

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

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

Ответы:


69

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

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

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

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


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

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

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

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

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

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

34

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

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

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

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

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

21

TL; DR;

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

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

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

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
}

—Конечный пример]

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

Если мы посмотрим на предложение A Proposal to Add Y Combinator to Standard Library, оно дает рабочее решение:

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 :

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

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

Здесь 'fib' является эквивалентом лямбда-выражения * 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);
};

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

Другой естественный подход - использовать std :: function:

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

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

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


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

"" Если имя объекта с неопределенным типом заполнителя появляется в выражении, программа имеет неправильный формат "Я не вижу такого появления в программе. selfНе похоже на такой объект.
n. 'местоимения' м.

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

13

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

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
Приветствия и hth. - Alf

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

9

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

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(), как правило, не может быть выведен до его создания (путем вызова с некоторым аргументом некоторого типа). Так что ручная машинная перезапись кода на основе шаблонов работает хорошо.
Приветствия и hth. - Alf

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

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

7

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

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

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

#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передается в. Sensible. Но он упускает из виду плохо сформированный код.
Yakk - Адам Неврамонт

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

конечно; но любое выражение, тип которого определяется неполным выводом типа возвращаемого значения, остается незаконным. Просто некоторые компиляторы ленивы и не проверяют их позже, когда все работает.
Якк - Адам Неврамонт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.