C++ での関数引数の遅延評価

怠け者になることもあります。やらなければならないことはわかっていても、まだやりたくない場合があります .今それを行う必要はありません。後で行う必要があります.そして、後で、作業全体を行う必要がないことが判明するかもしれません. 今すぐやりたい 、必要以上の作業を行う可能性があります。

同じことがコードにも当てはまります。必要でなくても実行することがあります。計算にコストがかかるいくつかの引数を渡して関数を呼び出すと、関数は他の引数のためにそれらのすべてを必要としません.Wouldn'実際に必要なときにのみ引数を計算するのは素晴らしいことですか?

これは関数引数の遅延評価と呼ばれ、このブログ投稿では C++ で行う方法を紹介しています。

モチベーション

メッセージをログに記録する関数を考えてみましょう:

logger.debug("Called foo() passing it " + std::to_string(argument_a)
 + " and " + std::to_string(argument_b));
…

ロガーには、「デバッグ」、「警告」、「エラー」などのさまざまなログ レベルがあります。これにより、実際にログに記録される量を制御できます。上記のメッセージは、ログ レベルが「デバッグ」レベルに設定されている場合にのみ表示されます。 .

ただし、表示されていない場合でも、文字列は作成されてから破棄されます。これは無駄です。可能な修正は、必要になるまで文字列の作成を遅らせることです:

logger.debug("Called foo() passing it ", argument_a,
 " and ", argument_b);
…

現在、文字列はログに記録される前にのみフォーマットされるため、メッセージがログに記録されない場合、文字列はフォーマットされません。ただし、 215 の場合、引数は引き続き評価されます は、それ自体が高価な式であり、計算する必要があります。遅延関数の引数評価では、それを行う必要はありません。

目標

この投稿のために、より単純なケース 228 を考えてみましょう (私の 232 のうち この関数は、オプションまたは指定されたフォールバック値に含まれる値を返します。簡単な実装は次のようになります:

template <typename U>
T optional<T>::value_or(U&& fallback)
{
 if (has_value())
 return value();
 return static_cast<T>(std::forward<U>(fallback));
}

私たちの目標は、241 の遅延評価を実装することです。;このように呼ぶと:

auto result = opt.value_or(foo());

259 結果が実際に必要な場合にのみ呼び出す必要があります。つまり、 265 値を保存しません。

テイク 1:マクロ

簡単な解決策は、関数の代わりにマクロを使用することです。マクロには、実際にはすべてを評価するのではなく、式を関数本体に貼り付けるだけであるという「優れた」「機能」があります。

したがって、次のように動作します:

#define VALUE_OR(opt, fallback) \
 [&](const auto& optional) { \
 if (optional.has_value()) \
 return optional.value(); \
 using T = std::decay_t<decltype(optional.value())>; \
 return static_cast<T>(fallback); \
 }(opt)

アイデアは、新しい 277 を作成することです フォールバック値として必要な各式の関数。これは、指定された 287 を実行するラムダを作成することによって実現されます :値を返すか、何かを計算してそれを返します。ラムダは、指定されたオプション オブジェクトですぐに呼び出されます。

呼び出しは次のようになります:

auto result = VALUE_OR(opt, foo());

ただし、これは完全にマクロに依存しているため、改善を試みましょう。

テイク 2:ラムダ

前のマクロは、遅延評価したい特定の機能 (297) と密接に結合していました。 .分離してみましょう:機能を記述し、遅延評価式を渡します。

遅延評価式を作成するにはどうすればよいですか?

ラムダを使用します。通常どおり呼び出す代わりに、引数を返すラムダを指定します:

auto result = opt.value_or([&] { return foo(); });

308 の実装 - 遅延評価と非遅延評価の両方をサポートします - 次のようになります:

// normal implementation
template <typename U,
 typename = decltype(static_cast<T>(std::declval<U>()))>
T optional<T>::value_or(U&& fallback)
{
 if (has_value())
 return value();
 return static_cast<T>(std::forward<U>(fallback));
}

// lazy evaluation
template <typename U,
 typename = decltype(static_cast<T>(std::declval<U>()()))>
T optional<T>::value_or(U&& lambda)
{
 if (has_value())
 return value();
 return static_cast<T>(std::forward<U>(lambda)());
}

最初のオーバーロードは式をキャストするだけで、2 番目のオーバーロードはラムダを呼び出してその結果をキャストします。奇妙な 315 SFINAE に使用されます。323 内の式の場合 整形式である場合、オーバーロードが考慮されます。式は、そのオーバーロードに期待される動作です。

呼び出しはラムダで少し醜いですが、マクロを使用して改善できます:

#define LAZY(Expr) \
 [&]() -> decltype((Expr)) { return Expr; }

これは、参照によってすべてをキャプチャして式を返すラムダを作成するだけです. 335 の周りの二重括弧に注意してください .341353 どちらも同じ型 363 を生成します 、しかし 376 の場合 、 387 397 が得られます と 406 410 を生成します 、ここで参照を取得したいと思います。

次に、使用法は次のようになります:

auto result = opt.value_or(LAZY(foo()));

テイク 3:邪魔にならないようにする

前のアプローチは機能しますが、アルゴリズムの実装者にいくらかの作業が必要です。邪魔にならないようにして、いつ遅延評価を行うかを呼び出し元に任意に決定させることができればいいと思いませんか?

これは、特別なタイプ 423 を導入することで実行できます ラムダをアルゴリズムに渡す代わりに、432 マクロは、型に変換可能な特別なオブジェクトを作成できます。その変換は式を評価します。

これは次のようになります:

template <class Lambda>
class lazy_eval
{
 const Lambda& lambda_;

public:
 lazy_eval(const Lambda& lambda)
 : lambda_(lambda) {}

 lazy_eval(const lazy_eval&) = delete;
 lazy_eval& operator=(const lazy_eval&) = delete;

 using expression_type = decltype(std::declval<Lambda>()());

 explicit operator expression_type() const
 {
 return lambda_();
 }
};

ラムダへの参照を格納するだけで、 449 があります ラムダの結果を返す変換演算子です。455 を少し変更するだけです。 マクロ:

#define LAZY(Expr) \
 lazy_eval([&]() -> decltype((Expr)) { return Expr; })

これは、ラムダ式の型を明示的に渡すことができないため、必要なボイラープレート make 関数を節約する C++17 クラス テンプレート引数推定を使用します。

しかし、それで元の 466 関数…

template <typename U>
T optional<T>::value_or(U&& fallback)
{
 if (has_value())
 return value();
 return static_cast<T>(std::forward<U>(fallback));
}

… 次のように使用できます:

auto a = opt.value_or(42); // non-lazy
auto b = opt.value_or(LAZY(foo())); // lazy

470 マクロは、実装が 485 を行うすべての場所で使用できるようになりました 実装が暗黙の変換に依存している場合、または問題の関数がテンプレート化されていない場合は機能しませんが、これはコンパイル エラーによって検出されます。 496 結果が実際に必要な場合。この 509 怠惰に動作しません:

template <typename U>
T optional<T>::value_or(U&& fallback)
{
 T result(std::forward<U>(fallback));
 if (has_value())
 return value();
 return result;
}

しかし、とにかく、それはいくぶんばかげた実装です。

評価

これで、遅延引数評価の非侵入的で使いやすい実装が実装されました。しかし、それは実際にどの程度有用なのでしょうか?

すでに指摘したように、これは非侵入的ではなく、実装に依存してレイト キャストを実行します。また、実装がまったくキャストしない場合やテンプレート化されていない場合も機能しません。

さらに、適切なインターフェイスを作成するためにマクロに依存しています。また、マクロに依存するインターフェイスは、通常はお勧めできません。

511の場合 最良の解決策 - フォールバックの遅延評価が必要な場合 - おそらく単純に 525 を提供することです レイジー マクロを使用せずにラムダまたは Take 2 実装を使用するオーバーロード。遅延評価をいじる最初の動機は、「値を与えるか、この例外をスローする」メカニズムを提供することでした。これは 537 これは 545 で実行できますが、 、明らかではありません。

したがって、type_safe については、おそらく 552 を提供するだけでよいでしょう。 関数またはそのようなもの。

ただし、評価を遅らせるためにラムダを使用するこの手法は非常に有用であることに注意してください。コンパイル時の定数によってアサーションを制御できるようにするために、debug_assert ライブラリでこれを行いました。このブログ投稿で詳しく説明しました。

結論

関数パラメーターの遅延評価は、特定の状況で役立ちます。ラムダ式を使用し、それらをマクロの背後に隠すことで、C++ でそれを実現できます。

ただし、本番コードでこのように実際に使用することはお勧めしません。ほとんどの場合、より良い解決策は、アルゴリズムが遅延して機能するように設計することです。たとえば、範囲 v3 は、次のように遅延評価される無限範囲で機能します。

Haskell などの言語も遅延評価され、D には関数パラメーターの遅延ストレージ クラスがあることに注意してください。