Pull to refresh

Comments 45

Замыкания в C++ являются обычными объектами функций.

Расскажите поподробнее, что подразумевалось в этой строке? В оригинале написано так:
Closures are just function objects in C++

Если речь о том, что лямбда — это объект безымянного класса, в котором переопределен operator () , то лучше перевести «объект-функтор». Но тогда при чем тут «замыкания»?
Если же речь о том, что лямбды в C++ захватывают объекты из контекста либо по значению, либо по ссылке, то при чем тут функции?
Расскажите поподробнее, что подразумевалось в этой строке?

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

Если же речь о том, что лямбды в C++ захватывают объекты из контекста либо по значению, либо по ссылке, то при чем тут функции?

Нет, речь точно не об этом.

Я немного изменил формулировку, теперь лучше?
Спасибо.
Полез в стандарт и выяснил для себя:
Лямбда-выражение (lambda-expression) — это название конкретного выражения (синтаксиса) в языке. Это выражение описывает, как получить объект-замыкание (closure-object).
Теперь всё встало на свои места.
auto range = fromto(0, 10);
range(); // 0
range(); // 1

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

auto unit = [](auto x) {
return [=]() { return x; };
};
Воспринимайте это как «low-level». Видить такой код (при качественной организации архитектуры) вы должны не чаще, чем заглядывать в исходники CPython, CRuby или node.js.
Это не отменяет того факта, что синтаксис перегружен.
Как указали выше, вы вправе выбрать наиболее подходящий для ваших целей стиль написания.

Существуют ситуации, когда необходимо явно указывать все что можно, везде где можно. И эту возможность C++ предоставляет.
Я вам еще раз поясняю, что даже в самом «простом» стиле все выглядит ужасно. Либо код превращается в «си со строками» и все новомодные приблуды непонятно кому нужны.
Я не готов обсуждать «чистоту» синтаксиса на примере выше. Нужен конкретный пример.

Лично я имел дело с лямбдами только когда писал приложение для gstreamer. Лямбды были исключительно удобны в качестве замены коллбэкам.
Да при чем тут колбеки? Сами по себе лямбды нужны и полезны.
Речь про то, что их синтаксис излишне сложен. Как все остальное. Приходится писать гору ненужных закорючек, все эти [=](){};
Учитывая требования и возможности языка он очень даже лаконичен. Ну сравните например с костылями в ObjC в виде переопределения всех переменных с ключевым словом __block для измнения правил передачи. Или разнообразные global и local в python.
Про ObjC я к счастью ничего не знаю. С питоном сравнивать тоже как-то не очень, совсем из другой оперы. Но вот тот же питон читается даже человеком который его видит первый раз.
Хороший пример как надо — это хаскель.
лучше бы добавили возможно создавать замывкания в динамической памяти — сейчас любая поптыка «обернуть» замыкание или сделать указатель на него приводит к двойному копированию захваченных данных. Т.е. первый раз данные копируются внутрь замыкания (что не избежно), а второй — локальный объект замыкания копируется конструктором обёртки в объект из динамической памяти. Что немного глупо. Да, «используйте умные указатели 2 и т.п. Но выглядит, как костыль и каждый раз совершается дурная работа.
А лямбдаах удобно всякого рода многопоточную обработку и события делать.
Хотя всего-то нужно разрешить делать new [ ] {}
Скажите пожалуйста, а как сочетается с утверждением «замыкания не продлевают жизнь объектам, которые они используют» приведенный вами код:

auto fromto = [](auto start, auto finish) {   
  return [=]() mutable {     
    if(start < finish)       
      return start++;     
    else       
      throw std::runtime_error(“Complete");   
  }; 
};


Получается, что объект обращается к автоматическим переменным, со всеми вытекающими. Прокомментируйте пожалуйста подробнее.
В данном случае он не продлевает жизнь переменным start, finish, а копирует их, т.к. захват произошёл по значению ([=]).

То есть start в строке «return start++» — это уже не тот start что в строке «auto start, auto finish», это поле в классе, сгенерированном для внутренней лямбды.
Понятно, спасибо. Собственно, к копированию я и склонялся. С синтаксисом лямбд еще не знакомился, поэтому этот момент показался странным.
Здесь захват идёт по значению, о чем говорит знак равно "[=]". То есть для внутренняй лямбды будут созданы и использованы копии переменных, и никакой проблемы со временем жизни не существует.

В высказывании о продлении жизни имеется ввиду случай захвата по ссылке "[&]".

(прошу прощение за дублирующийся ответ)
Спасибо. Я правильно понимаю, что синтаксис [](){ ... } будет соответствовать варианту вообще без захвата окружения? То есть, это будет лямбда, но без замыкания. Налагает ли это какие-то дополнительные ограничения на использование?
Да, это вообще без захвата окружения. Можно также указывать конкретные переменные, например [foo], [&bar], [=, &baz].
Это всё не может не радовать. Жаль только, что почти во всех современных компиляторах малейшая синтаксическая ошибка при использовании современных фишек выливается в маленькую поэму из ошибок компиляции =/
Генерирование адекватных ошибок компиляции — это очень сложная тема, сравнимая по сложности с разработкой самого компилятора. Одной из целей при проектировании Clang ставили как раз адекватную информацию об ошибках, но даже там имеется много оговорок.

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

При компиляции и оптимизации программы, компилятор производит большое количество преобразований над графом управления. Это приводит к тому, что фактический код, подвергающийся трансляции в машинные коды изменяется до неузнаваемости. Если ошибка происходит где-то на поздних уровнях, восстановить справедливость оказывается очень сложно. Особенно это касается не ошибок а предупреждений. Многие недоумевают, почему бы самому компилятору не давать предупреждения вроде того что делает например PVS Studio? По той же причине.

Могу посоветовать почитать пост из блога LLVM, где описываются положительные стороны успешного восстановления после ошибок. Это даст представление о сложности.
Если честно, статья невнятная из-за искусственных примеров. Либо статья рассчитана опытных в функциональном программировании людей, которых просто интересует синтаксис реализации знакомых им конструкций в С++.

И ещё немного жаль, что ничего не написано о том, как всё это реализовано. Соответственно, какие получаются накладные расходы на эти вызовы, какие ограничения и т.д. Всё-таки, выбор именно С++ бывает связан с тем, что ты примерно представляешь, как все твои вызовы и данные «ложатся» на ассемблерный код.
bind и unit — это не монадЫ, а монадА, — а именно, Id.
Не, ну можно же было изобразить и какую-нибудь ещё монаду, да хотя бы Maybe :)
Боюсь, вот эта часть: auto; — самая сложная…
Ехал auto через auto, сунул auto auto в auto, auto auto auto auto.
Похоже, что к версии C++20 слово auto объявят устаревшим, и автовывод типов будет подразумеваться при отсутствии явно указанного типа. Просто, если убрать слово auto то все примеры выглядят куда как красивее.
В Комитете сейчас обсуждается предложение сделать именно так для range-for. Т.е. вот такое:
for (x: xs) { ... }

будет эквивалентно текущему:
for (auto&& x: xs) { ... }

Тут, правда, есть вполне прагматическая причина. Как показала практика, народ, дорвавшить до range-for и auto, начинает писать так:
for (auto x: xs) { ... }

Это в принципе работает, но есть две грабли. Первая — происходит ненужное (в 99% случаев) копирование элементов контейнера в локальную переменную. На чем-нибудь вроде строк это может неплохо просадить производительность. И вторая — когда человек пытается в теле цикла поменять x; это молча компилируется, но меняется именно скопированное значение, а люди обычно ожидают изменения элемента непосредственно в контейнере. Вариант с auto&& решает все эти проблемы, и автоматически подстраивается под const-ность контейнера, поэтому его было решено сделать «умолчательным», а синтаксиса лучше, чем просто выкинуть тип, не придумапи.
А в чём преимущество rvalue в данном контексте?
Не работает с прокси-итераторами (vector).

Вообще, лучше почитать сам N3853, там половина текста — это почему надо делать именно так, а не иначе :)
Для тех кто полез выяснять почему же auto& хуже чем auto&&, int18h на самом деле имел ввиду vector<bool>, хабрапарсер видимо сфейлил.
Да, именно так, спасибо за поправку (сфейлил я сам, забыв про разметку).
«Мы взяли один из самых запутанных языков, добавили в него простые и элегантные концепции из других запутанных языков, но чтобы придерживаться духу языка сделали их синтаксис еще более запутанным.»
А тем временем, писать и совершенствовать компиляторы для C++ становится всё сложнее. Вот будет хохма, если к созданию ИИ людей насильно подтолкнёт необходимость в очередной раз проапгрейдить C++.
Ну да. И в какой-то момент писать и компилировать плюсы сможет только Lisp c:
Sign up to leave a comment.

Articles