2017-01-31 4 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のうちの1つは、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))...fargsのこれらのコピーは、 という遅延機能を構成します。遅延関数の呼び出しは、gDECAY_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)異なるスレッドからラムダを2回呼び出すかもしれないという意味で、あなたのcached_lambdaはスレッドセーフではありません。また、 'calculated'を' true'に設定するのを忘れました(少なくともその部分を編集します)。 (2)しかし、未来が実際に実行されるとき、私はどのような保証をしていますか?それが実際に怠け者であることをどのように知っていますか? – einpoklum

+1

@einpoklum "タスクは、その結果が最初に要求されたときに呼び出しスレッドで実行されます(遅延評価)" - http://en.cppreference.com/w/cpp/thread/launchから引用します。標準での確認 – alexeykuzmin0

+0

@einpoklumあなたは(1)について正しいですし、 'std :: future'を使うもう一つの理由です。 – alexeykuzmin0

4

ここではいくつかのことが起こっています。

Applicative order評価とは、引数を関数に渡す前に評価することを意味します。 Normal order評価は、評価する前に引数を関数に渡すことを意味します。

通常の順序評価には、いくつかの引数が評価されないという利点があり、いくつかの引数が何度も何度も評価されるという欠点があります。

Lazy評価は、通常、normal order + memoizationを意味します。あなたが一度も評価する必要はないが、必要な場合は、その結果を覚えておいてほしいという希望で、評価を延期する。重要なのは、用語を決して一度も一度評価することではなく、これを提供する最も簡単なメカニズムはメモ帳です。

promise/futureのモデルが再び異なります。ここでの考え方は、十分な情報が得られるとすぐに、別のスレッドで評価を開始することです。その後、できるだけ長い間その結果を見て、すでに利用可能である可能性を向上させます。


promise/futureモデルは、遅延評価といくつかの面白いシナジーがあります。戦略が行く:

  1. 結果は間違いなく
  2. スタートの評価が
  3. 別のスレッドに行く必要となるまで、いくつかの他のもの
  4. を行い、バックグラウンドスレッドが結果を完了し、保存する評価を延期どこか
  5. 初期スレッドは結果を取得します

結果がきれいに紹介されるバックグラウンドスレッドによって生成されます。

2つのシナジー効果にもかかわらず、同じ概念ではありません。

+0

さて、私が仮定したように評価を開始しない、先物のための['std :: launch :: deferred](http://en.cppreference.com/w/cpp/thread/launch)はどうでしょうか?質問ではなく、必要になるまで待ちます。それはまた、約束/将来のモデルの一部または一側面です。 ...または - 文章ではなくC++実装のそれとまったく同じですか? – einpoklum

+0

特にそれはどうですか? std :: launchは非同期または遅延評価を提供しますが、現在のAPIを介して必要な場合はasync-eval-only-onlyをサポートしません。 –

+0

関数型プログラミングでは、代わりに "共有"と呼ばれる "メモ"と呼ばれるものを聞いたと思います。これと比較して、「メモ化」は、関数の戻り値を保存し直して再計算されないようにするテクニックです。 'fib(n)= fib(n-2)+ fib(n-1)'を(exptimeの代わりに)線形アルゴリズムに変換します。だから、 "メモ"は、私が聞いたことから、ダイナミックなプログラミングにもっと似ています。それでも、両方のアプローチが後でアクセスできるキャッシュに計算結果を保存しているという意味で正しいです。 (+1) – chi