Подтвердить что ты не робот

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

Я только что прочитал:

Lazy Evaluation в С++

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

auto x = foo();

вы выполняете

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

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

auto x = unevaluted_x();

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

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

4b9b3361

Ответ 1

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

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, реализации должны отложить вызов или выбрать политику, когда не более concurrency можно эффективно использовать. -end note]

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

Ответ 2

Здесь происходит несколько вещей.

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

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

Оценка

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

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


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

  • Отложить оценку до тех пор, пока результат не будет определен.
  • Начать оценку в другом потоке
  • Сделайте некоторые другие вещи
  • Фоновый поток завершается и сохраняет результат где-то
  • Исходный поток извлекает результат

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

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