読者です 読者をやめる 読者になる 読者になる

ボレロ村上 - ENiyGmaA Code

中3女子です。

bind の可変長プレースホルダその他を実装した

中3女子です。


今回はみんな大好き bind について。
bind で関数に引数に値を束縛したり引数の順序を組み替えたりする手法は、C++ ではかなり以前から存在する。
Boost.Bind に始まって Boost.Lambda, Boost.Phoenix, また C++11 の標準ライブラリにも導入されている。
また、標準ライブラリ相当かつ constexpr な bind を、Sprout でも実装している。


使い方は簡単で、標準ライブラリとほぼ同じである。

#include <sprout/functional.hpp>

constexpr auto f = sprout::bind( sprout::minus<>(), 10, sprout::placeholders::_1 );
constexpr auto t = f( 3 );

この呼出は、当然 minus<>()( 10, 3 ) すなわち 10 - 3 と等価になる。


Sprout では、プレースホルダに関していくつか独自の機能を持っている。
そのうちのいくつかを解説する。

プレースホルダリテラル

using namespace sprout::placeholders::udl;

constexpr auto f = sprout::bind( sprout::minus<>(), 10, 1_ );
constexpr auto t = f( 3 );

上記の 1_ は、sprout::placeholders::_1 と同じである。
operator"" _() ユーザ定義リテラルは、引数の整数に対応するプレースホルダを生成する。
プレースホルダリテラルを使う利点は、_1, _2, _3,... といった定義済みプレースホルダを使うのと違って、実装依存の数の制限がないことだ。
例えば _999 という定義済みプレースホルダは存在しないが、プレースホルダリテラルを使って 999_ というプレースホルダを生成することは合法で容易だ。

ポジショナルプレースホルダ

ポジショナルプレースホルダは、書かれた順序によってインデックスが決まるプレースホルダである。

using sprout::placeholders::_;

constexpr auto f = sprout::bind( sprout::minus<>(), 10, _ );
constexpr auto t = f( 3 );

上記の _ はポジショナルプレースホルダであり、この場合は sprout::placeholders::_1 に置換される。
例えば bind( fn, _, _, _ ) という式があった場合、これは bind( fn, _1, _2, _3 ) と同じになる。


このような処理は、bind 時に束縛される引数の型リストを TMP によって置き換えることで実現されている。

可変長プレースホルダ

可変長プレースホルダは、可変長関数を使う際に非常に重要である。

struct ArgCount {
     template<typename... Args>
     constexpr int operator()(Args&&...) const { return sizeof...(Args); }
};

using sprout::placeholders::_va;

constexpr auto f = sprout::bind( ArgCount(), _va );
constexpr auto t = f( 1, 2, 3, 4, 5 );
static_assert( t == 5, "" );

上記の _va が可変長プレースホルダであり、これは実際の呼出時に渡されるすべての引数を受け取ることができる。
また、_va は他の任意のプレースホルダと組み合わせたり複数書くこともできる。
例えば bind( fn, _va, _va )( args... ) は、fn( args..., args... ) と等価になる。


呼出時に TMP を駆使して、可変長プレースホルダプレースホルダの列に展開した型リストを生成することで、こうした処理を実現している。
このような可変長プレースホルダの機能は、確認してみた限りでは Boost.Bind, Boost.Lambda, Boost.Phoenix や標準ライブラリのいずれでも実装されていないので、
Sprout の先進的な機能の一つといえるだろう。

注意点

Sprout.Bind をコンパイル時に使う場合に、constexpr 特有の注意点が一つある。
コンパイル時において、bind した返値から直接関数呼び出ししたり、constexpr アルゴリズムに渡してはならない。

using sprout::placeholders::_;

constexpr t = sprout::bind( sprout::minus<>(), 10, _ )( 3 ); // error!
constexpr a = sprout::range::transform<sprout::array<10, int> >(
    sprout::array<10, int>{{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }},
    sprout::bind( sprout::multiplies<>(), 10, _ )
    ); // error!

これは、以前【C++11の糞仕様と戦ってアクセッサをconstexprにする - ボレロ村上 - ENiyGmaA Code】で書いた仕様上の問題に起因する。
bind の返値の operator() は、非cv版、const版、volatile版、cv版 がオーバーロードされており、
const オブジェクトからの呼出は、非 constexpr である非cv版が優先して呼ばれてしまうからだ。


この問題を回避するには、bind ではなく cbind を使うようにする。

using sprout::placeholders::_;

constexpr t = sprout::cbind( sprout::minus<>(), 10, _ )( 3 ); // OK!
constexpr a = sprout::range::transform<sprout::array<10, int> >(
    sprout::array<10, int>{{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }},
    sprout::cbind( sprout::multiplies<>(), 10, _ )
    ); // OK!

cbind の返値の operator() は、const版、cv版 すなわち constexpr 関数のみがオーバーロードされているため、問題が起きない。

開発上の雑感

この Sprout.Bind の実装にはかなり苦労した。
Sprout のサポート範囲である GCC4.7 以上と Clang3.2 以上のすべてで問題なく動作するようになったのは、実のところつい最近である。


というのも、実装のロジックはずっと以前からできていたのだが、ここ→(コンパイラ性能比較 - Sprout C++ Library Wiki)に纏めたような処理系のバグを見つけ出し、
それらに対処するワークアラウンドを書く必要があったからだ。


そのうち Boost.Lambda のようなもっと記述性の高いファンクショナルライブラリに拡張したいところだが、
constexpr 本 のほうも書き進めていい加減目処を付けたい。
さて、どうしたものか。