【C++】ラムダのオートクロージャ化を実現するテクニック

記述が面倒なラムダ式を用いることなく、より簡潔な記法でクロージャの生成を実現する方法を紹介します。次のような記述を実現します。

オートクロージャ、部分適用

std::vector<int> v = {1, 2, 3, 4};

// Before
filter(v, [](int i) { return i < 3});
// After
filter(v, _ < 3); // v == {1, 2}

sort(v, _ > _);   // v == {2, 1}
// sort(v, [](int a, int b) { return a > b; });

_ < 3_ > _がラムダ式の処理部に相当します。今回紹介するテクニックによって、Swift言語の「@autoclosure」やScala言語の「名前渡し(俗にいう遅延引数 - lazy arguments)」に近い挙動の実現が可能となります。

目次

Lambdaオペランドによる条件式オブジェクトの実現

Lambdaクラスをオペランドとして解釈する特殊な演算子を多重定義することでこれを実現します。

/* autoclosure.h */

struct Lambda {};

template<class T> auto operator>(Lambda, const T& b) {
   return [&](const auto& a) { return a > b; };
}
#include <algorithm>     // std::any_of
#include "autoclosure.h" // Lambda

int main() {
   Lambda _;
   std::vector<int> v = {1, 2, 3};
   
   // any_of: 条件に合った要素を含むかどうかを判定
   std::any_of(v.begin(), v.end(), _ > 0); // true
   std::any_of(v.begin(), v.end(), _ > 9); // false
   std::any_of(v.begin(), v.end(), Lambda{} > 2); // true
}
アンダースコアの利用に関する注意

今回定義した演算子operator > (Lambda, T)はある種の部分適用を実現するものです。Tの値は、戻り値として定義されたラムダ式へと束縛されます。Lambda引数はプレースホルダに相当します。演算子が返すラムダ式のクロージャ・オブジェクトは任意のタイミングでの呼び出しが可能となります。

auto greater = _ > 1;
greater(2); // true

1は即時評価の後、_ >式へ部分適用される形となります。戻り値のクロージャオブジェクトとその変数greaterは任意のタイミングでの関数呼び出しが可能ですが、_ > 1式そのものが遅延評価されるわけではないため注意が必要です。

auto greater = _ > strlen("a"); // この行でstrlen関数が呼び出される
greater(2); // この行でstrlen関数が呼ばれるわけではない

そういう意味では一般的なオートクロージャや遅延引数とは少し異なる代物になるのですが、簡潔な記述を実現することは可能となります。要するに本イディオムは部分適用を簡潔に表現するためのイディオムとなります。

必要に応じて>以外の演算子をオーバーロードします。また複数の引数を受け取るクロージャ・オブジェクトを返すことも可能です。

auto operator>(Lambda, Lambda) {
   return [](const auto& a, const auto& b) { return a > b; };
}
std::vector<int> v = {1, 2, 3};
std::sort(v.begin(), v.end(), _ > _);
// v == {3, 2, 1}

ショートハンド引数の実現

ショートハンド引数(Shorthand Argument Names)の実現方法を紹介します。

sort({1, 3, 2}, $0 > $1); // {3, 2, 1}
sort({1, 3, 2}, $1 > $0); // {1, 2, 3}
ドルマークの利用に関する注意

$0が第一引数、$1が第二引数に相当します。_は従来通り、ワイルドカード引数(Wildcard Parameter)として機能します。

struct Lambda {
   int order = -1;
};

inline auto operator+(Lambda L, Lambda R) {
   if (-1 == L.order) L.order = 0;
   if (-1 == R.order) R.order = 1;
   return [=](const auto& a, const auto& b) {
      return L.order ? R.order ? b + b : b + a
      /**/           : R.order ? a + b : a + a;
   };
}
const Lambda _{}, $_{-1}, $0{0}, $1{1};

std::string a = "A", b = "B";
assert( "AA" == ($0 + $0)(a, b) );
assert( "AB" == ($0 + $1)(a, b) );
assert( "BA" == ($1 + $0)(a, b) );
assert( "BB" == ($1 + $1)(a, b) );
assert( "AB" == ($_ + $_)(a, b) );

Lambdaオペランドはconst修飾された定数として定義しましょう。多重定義された演算子(operator + (Lambda, Lambda))側で、Lambda型メンバ変数orderの値を基準としたコンパイラ最適化(条件式の排除、インライン化等)の施行が期待できます。

ドルマークの利用に関する注意

処理系によっては、識別子としてドルマーク(ドル記号、$)が利用できない場合があるため注意してください。実行環境によっては利用が禁止されている場合もあります。

int $ = 0; // error: '$' in identifier [-Werror,-Wdollar-in-identifier-extension]

中には拡張機能や固有の仕様としてドルマークの利用を許可しているコンパイラもありますが(Microsoft Visual C++の付属コンパイラ等)、C++の言語仕様上では、ドル記号は、識別子としての利用が可能な文字の有効範囲に含まれていません。これはC言語でも同様ですので注意してください。

アンダースコアの利用に関する注意

アンダースコアで始まる識別子は、グローバル空間では予約済みとなっているため利用できません。ただし、グローバル空間に影響しない個別の空間(関数内、構造体内部、名前空間内、等)であれば、利用は可能となっています。ただし、アンダースコアに続いて大文字で始まる識別子については、予約済みとなっています。またダブルアンダースコア(下線を2つ並べた状態)を含む識別子も同様に予約済み識別子となりますので注意が必要です。

広告