C++14/17中的惰性求值——只有lambda还是也有future等?

14 浏览
0 Comments

C++14/17中的惰性求值——只有lambda还是也有future等?

我刚读到:

C++中的惰性求值

我注意到这篇文章有点旧,大多数答案都是关于2011年之前的C ++。如今我们有语法上的lambda表达式,甚至可以推断出返回类型,因此惰性求值似乎归结为只需传递它们:而不是

auto x = foo();

你执行

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

然后在需要时/何处进行评估:

auto x = unevaluted_x();

似乎没有更多了。但是,其中一个答案建议使用具有异步启动的futures。有人能够解释一下为什么/如果future在惰性求值工作中很重要吗,在C ++或更抽象地方面?似乎futures可能会急切地评估,但只是在另一个线程中,并且也许比创建它们的任何其他对象的优先级低;不管怎样,这应该与实现有关,对吧?

此外,在惰性求值的背景下,还有其他有用的现代C ++结构需要考虑吗?

admin 更改状态以发布 2023年5月22日
0
0 Comments

这里有几件事情。

应用序评估意味着在将它们传递给函数之前评估参数。正常序评估意味着在将参数传递到函数之前评估它们。

正常序评估的好处是一些参数永远不会被评估,缺点是有些参数会被评估多次。

惰性评估通常意味着正常序+记忆化。延迟评估,希望你根本不需要评估,但如果你需要,记住结果,这样你只需要做一次。重要的部分是永远或只有一次评估一个术语,记忆化是提供这个机制最简单的方法。

再来看一下promise/future模型。这里的想法是在有足够信息可用时开始评估,可能在另一个线程中,尽可能长时间地留下结果的查看,以提高它已经可用的机会。


promise/future模型与惰性评估有一些有趣的协同作用。策略如下:

  1. 延迟评估,直到结果确实是需要的
  2. 在另一个线程中开始评估
  3. 做一些其他的事情
  4. 后台线程完成并将结果存储在某个地方
  5. 最初的线程检索结果

当结果由后台线程生成时,记忆化可以被巧妙地引入。

尽管两者有很强的协同作用,但它们并不是相同的概念。

0
0 Comments

当你编写代码时

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

每次调用unevaluated_x来获取这个值时都会进行计算,浪费计算资源。因此,为了避免过度的工作,有必要跟踪 Lambda 是否已经被调用过(可能是在另一个线程中或者在代码库的完全不同位置)。为此,我们需要对 Lambda 做一些包装:

template
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))DECAY_COPY(std::forward(args))...存储在共享状态中。这些fargs的副本构成了延迟函数。推迟函数的调用计算INVOKE(std::move(g), std::move(xyz)),其中gDECAY_COPY(std::forward(f))的存储值,xyzDECAY_COPY(std::forward(args))...的存储副本。任何返回值都将作为结果存储在共享状态中。在执行推迟函数期间被传播的任何异常都将作为异常结果存储在共享状态中。直到函数完成后,共享状态才准备就绪。对于指向该共享状态的异步返回对象的第一次非定时等待函数调用(30.6.4),应在调用等待函数的线程中调用推迟函数。一旦开始评估INVOKE(std::move(g),std::move(xyz)),该函数将不再被视为延迟。[注意:如果将此策略与其他策略一起指定,例如在使用launch::async | launch::deferred的策略值时,实现应推迟调用或选择策略,直至无法有效利用更多并发性为止。——笔者注]

因此,你可以保证在需要之前不会调用计算的过程。

0