2017-01-31 11 views
13

Я только что прочитал:Ленивая оценка в C++ 14/17 - просто лямбды, а также фьючерсы и т. Д.?

Lazy Evaluation in C++

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

auto x = foo(); 

выполнение

auto unevaluted_x = []() { return foo(); }; 

, а затем оценить, когда/где вам нужно:

auto x = unevaluted_x(); 

Похоже, в этом нет ничего более. Однако один из answers there предлагает использовать futures с асинхронным запуском. Может кто-то изложить, почему/если фьючерсы значимы для ленивой оценки, на C++ или более абстрактно? Кажется, что фьючерсы вполне могут быть оценены с нетерпением, но просто, скажем, в другом потоке и, возможно, с меньшим приоритетом, чем все, что их создало; и в любом случае, это должно быть зависимым от реализации, не так ли?

Кроме того, существуют ли другие современные C++-конструкции, которые полезно учитывать в контексте ленивой оценки?

+1

фьючерсы ждут результата какого-то (возможно асинхронного) процесса. Они одноразовые и довольно тяжеловесные. Если вы ищете ленивую оценку в той же теме, возможно, не то, что вам нужно. Существует библиотека под названием boost.outcome, которая разрабатывается. Это, по сути, легкие фьючерсы (не предназначенные для кросс-ниточной работы). Если вы хотите многократно называть свою ленивую функцию, возможно, подходящим является объект функции или лямбда. Вы также можете посмотреть на boost.hana или подобное. –

ответ

12

Когда вы пишете

auto unevaluted_x = []() { return foo(); }; 
... 
auto x = unevaluted_x(); 

Каждый раз, когда вы хотите, чтобы получить значение (при вызове unevaluated_x) он вычисляется, тратить вычислительные ресурсы. Таким образом, чтобы избавиться от этой чрезмерной работы, неплохо было бы отслеживать, была ли уже названа лямбда (может быть, в другом потоке или в другом месте в кодовой базе). Для этого нам нужна обертка вокруг лямбда:

template<typename Callable, typename Return> 
class memoized_nullary { 
public: 
    memoized_nullary(Callable f) : function(f) {} 
    Return operator()() { 
     if (calculated) { 
      return result; 
     } 
     calculated = true; 
     return result = function(); 
    } 
private: 
    bool calculated = false; 
    Return result; 
    Callable function; 
}; 

Пожалуйста, обратите внимание, что этот код является только примером и не поточно.

Но вместо того, чтобы изобретать колесо, вы могли бы просто использовать std::shared_future:

auto x = std::async(std::launch::deferred, []() { return foo(); }).share(); 

Это требует меньше кода писать и поддерживает некоторые другие функции (например, проверить, является ли уже вычислено значение, безопасность потоков, и т.д).

Там в следующий текст в стандарте [futures.async, (3,2)]:

Если launch::deferred установлен в политике, магазины DECAY_COPY(std::forward<F>(f)) и DECAY_COPY(std::forward<Args>(args))... в общем состоянии. Эти копии f и args составляют отложенную функцию. Вызов функции отсрочки оценивает INVOKE(std::move(g), std::move(xyz)), где g - сохраненное значение DECAY_COPY(std::forward<F>(f)) и xyz - сохраненная копия DECAY_COPY(std::forward<Args>(args))..... Любое возвращаемое значение сохраняется в результате совместного использования в результате . Любое исключение, распространяемое от выполнения отложенной функции , сохраняется как исключительный результат в общем состоянии. Общее состояние не создано , пока функция не завершится.Первый вызов функции времени ожидания (30.6.4) в асинхронном объекте возврата, ссылающемся на это состояние общего доступа, должен вызывать отложенную функцию в потоке, который вызвал функцию ожидания. Как только начинается оценка INVOKE(std::move(g),std::move(xyz)), функция больше не считается отложенной. [Примечание. Если эта политика равна , указанной вместе с другими политиками, например при использовании значения политики launch::async | launch::deferred, реализации должны отложить вызов или выбрать политику, когда не будет эффективно использоваться. -End note]

Итак, у вас есть гарантия, что расчет не будет вызываться до того, как он понадобится.

+0

(1) Ваш cached_lambda не является потокобезопасным, в том смысле, что вы можете дважды называть лямбда из разных потоков. Кроме того, вы забыли установить 'calculate' в' true' (отредактируйте эту часть как минимум). (2) Но какие гарантии у меня есть, когда будущее действительно выполняется? Откуда я знаю, что это на самом деле лениво? – einpoklum

+1

@einpoklum "задание выполняется в вызывающем потоке при первом запросе его результата (ленивая оценка)" - процитировано по адресу http://en.cppreference.com/w/cpp/thread/launch. Будет обновлен ответ после нахождения подтверждение в стандарте. – alexeykuzmin0

+0

@einpoklum Вы правы насчет (1), и это дополнительная причина использовать 'std :: future'. – alexeykuzmin0

4

Здесь есть несколько вещей.

Оценка означает оценку аргументов перед передачей их в функцию. Normal order Оценка означает передачу аргументов в функцию перед их оценкой.

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

Lazy оценка обычно означает normal order + memoization. Отложите оценку в надежде, что вам вообще не нужно оценивать, но если вам нужно, запомните результат, поэтому вам нужно сделать это только один раз. Важная часть - это оценка термина никогда или один раз, memoization - это самый простой механизм для обеспечения этого.

Модель promise/future другая. Идея здесь заключается в том, чтобы начать оценку, возможно, в другом потоке, как только у вас будет достаточно информации. Затем вы оставляете взгляд на результат как можно дольше, чтобы улучшить шансы, что он уже доступен.


promise/future модель имеет некоторые интересные взаимодействия с ленивой оценкой. Стратегия идет:

  1. Перенести оценки, пока результат не будет, безусловно, будет необходимо
  2. Начните оценку идет в другом потоке
  3. ли некоторые другие вещи
  4. Фоновый поток завершает и сохраняет результат где-то
  5. Исходная нить получает результат

Память может быть аккуратно введена в результате производится фоновым потоком.

Несмотря на синергизм между ними, они не являются одной и той же концепцией.

+0

Ну, а как насчет ['std :: launch :: отложен] (http://en.cppreference.com/w/cpp/thread/launch) для фьючерсов, который не запускает оценку, как я предполагал в вопросе, а скорее ждет, пока они не понадобятся? Это также часть или аспект перспективной/будущей модели. ... или - это так же, как в реализации на C++, а не в литературе? – einpoklum

+0

Что относительно этого? std :: launch предлагает асинхронную или ленивую оценку, но не поддерживает async-eval-only-when-needed через текущий API. –

+0

В функциональном программировании, я думаю, я слышал, что вы называете «memoization», чтобы называться «совместное использование». Для сравнения, «memoization» - это метод сохранения возвращаемых значений функции, чтобы они не были повторно пересчитаны, возможно, путем поворота, например. 'fib (n) = fib (n-2) + fib (n-1)' в линейный алгоритм (вместо одного из них). Таким образом, «memoization» больше напоминает динамическое программирование, из того, что я слышал. Тем не менее, вы правы в том смысле, что оба подхода сохраняют результат вычисления в кеше, к которому впоследствии можно получить доступ. (+1) – chi