C++の遅延評価と部分適用|それと遅延評価式と部分適用式の実現

C++には遅延評価を実現するための機能は存在しませんが、ラムダ式やbind関数を用いることで、処理の実行を任意のタイミングに先延ばすことが可能になり、遅延評価に近い挙動を実現することが可能になります。本記事ではラムダ式やbind関数の両者による遅延評価と部分適用の実現方法や、注意点について解説します。

ラムダ式による遅延評価の実現

実引数の値評価と関数呼び出しの遅延評価を実現したい場合にはラムダ式の利用が適切です。なお厳密な遅延評価ではないため、評価時には明示的な関数呼び出しが必要となります(f())。なおラムダ式は[]() { 式 }という形式で利用します。

auto f = []() {
   printf("hello");
};

f(); // "hello"

このように、処理printf("hello")は、ラムダ式のオブジェクトfに対する関数呼び出しのタイミングで実行される事になります。

以下の例でも同様です。関数オブジェクトgの呼び出しと、文字列オブジェクトstd::string("99")の生成はf関数呼出し時に行われます。

auto g = [](auto v) {
   return std::stoi(v);
};
auto f = [g] {
   return g(std::string("99"));
};

assert( 99 == f() );

このように、ラムダ式によって文字列オブジェクトの生成を関数呼出しの直前まで先延ばしすることが可能になります。なお評価値のキャッシュはされないため注意してください。

遅延処理による遅延評価の実現

実はC++入出力ストリームには遅延処理のような、いわゆるバッファーの考え方が取り入れられています。たとえばstd::coutによる出力処理はstd::endlstd::flushが渡されたときに始めて、画面上に文字を出力するような働きをします。

std::cout << 9 + 1;     // まだ画面上には表示されない
std::cout << 8;         // まだ
std::cout << std::endl; // この段階で始めて画面に表示される
// 出力結果: "108\n"

もっとも、この場合は9 + 1の演算処理が遅延評価されるのではなく、出力処理自体が遅延されることになります。

演算や計算を遅延処理したい場合には、一時オブジェクトやメンバ変数、ラムダ式を駆使して何とかすると良いでしょう。

struct Operand { int value; };
template<class Operator> struct Expression {
  Operator op; Operand a, b;
  operator int() { return op(a.value, b.value); }
};
auto operator+(Operand a, Operand b) {
  auto f = [](auto a, auto b) { return a + b; };
  return Expression<decltype(f)>{f, a, b};
}
auto expr = Operand{1} + Operand{2};
int i = expr; // このタイミングで`operator int()`が呼ばれ演算処理が行われる
assert( 3 == i );

上手くやれば文字列型の結合処理を遅延評価することもできます。必要に応じて評価値のキャッシュを行ったりするのも面白いかもしれません。

bind関数による部分適用の実現

C++標準ライブラリのstd::bindを用いることで、部分適用を実現することが可能となります。

auto f = std::bind(printf, "hello");
f(); // "hello"
     // 関数呼び出し`printf("hello")`が行われる

なお、bind関数の場合は実引数の評価が値束縛時に行われてしまうため、遅延評価を実現する機能としては不完全な点に注意が必要です。

auto f = std::bind(g, std::string("99")); // `std::string("99")`の評価値が束縛される
assert( 99 == f() );
assert( 99 == f() ); // 毎回`g`関数が呼び出される

std::bind関数に関するより詳しい解説については以下のページが参考になります。

std::bind関数 - 引数の束縛と部分適用

部分適用式・遅延評価式の実現

bind関数はプレースホルダの指定が必要な点や、可変長引数に対応していない等の問題があります。ここではより使い勝手の良い部分適用関数の実現方法を紹介します。これによってbind関数では実現できなかった可変長引数の扱いも可能となります。また簡易的な遅延評価式としての利用も可能です。

auto f = _(puts, "hello");
f(); // "hello"

auto g = _(printf, "%s%d");
g("Shop", 99);   // "Shop 99"
g("DAISO", 100); // "DAISO 100"

このように_()という簡潔な記法でthunk(サンク)の生成を実現します。具体的な定義方法は次の通りです。

struct Lazy {
   template<class F, class... A> auto operator()(F&& f, A&&... a) {
      return [&](auto&&... b) {
         return std::forward<F>(f)(std::forward<A>(a)..., std::forward<decltype(b)>(b)...);
      };
   }
};

関数呼び出し式を任意のタイミングで呼び出すことが可能になります。

auto _ = Lazy{};

auto hello = _(puts, "hello world");
hello(); // `puts("hello world")`呼び出しが行われる
auto lazy = Lazy{};

auto set = std::make_tuple(
   lazy (puts, "Shop"),
   lazy (putchar, '9')
);
auto Q = std::get<1>(set);
std::get<0>(set)(); // "Shop"
Q(), Q();           // "99"

部分適用式/部分適用関数としての利用も可能です。

auto print = _(printf, "%s%d", "C++");
print(11); // "C++11"
print(14); // "C++14"
auto p = _(printf, "%s %d");
auto q = _(p, "Shop");
q(99); // "Shop 99"
_(_(_(printf, "%s %d"), "Shop"), 99)(); // "Shop 99"
_(_, _(_(_(printf, "%c%d"), 'C'), 99))()(); // "C99"

Coming Soon

C++17で利用可能になる予定のテクニックです。

//    THIS YEAR
//   Coming Soon
//  Aug  24, 2017

template<class T> struct print {
   T v; // print(T v) : v(v) {}
   void operator()() const { std::cout << v << std::endl; }
};
template<class T> print(T t) -> print<T>; // 推定ガイド

auto fn = print{99}; // テンプレート実引数推定
fn(); // "99"
template<class F, class... A> struct lazy {
   F f; std::tuple<A...> a; lazy(F f, A... a) : f(f), a(a...) {}
   auto operator()() const { return std::apply(f, a); }
};

auto fn = lazy { puts, "hello" };
fn(); // "hello"
広告