Лямбда возвращается сама: это законно? - PullRequest
0 голосов
/ 05 сентября 2018

Считайте, что это довольно бесполезная программа:

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

Ответы [ 6 ]

0 голосов
/ 06 сентября 2018

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

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, потому что external принимает 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.

0 голосов
/ 05 сентября 2018

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

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

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

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

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

0 голосов
/ 05 сентября 2018

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
}

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

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

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

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

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

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

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

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

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

Пример: * 1 052 *

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

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

0 голосов
/ 05 сентября 2018

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

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

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

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

<ч />

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

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

И что вы хотите:

auto it = y_combinator([&](auto self, auto b){
    std::cout << (a + b) << std::endl;
    return self;
});
0 голосов
/ 05 сентября 2018

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

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

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

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

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

0 голосов
/ 05 сентября 2018

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