2012年9月2日日曜日

(今更) C++ で拡張メソッド

Transactional Memory について予告をしておきながら別ネタ。何で実装しようと思ったのかそのきっかけを忘れてしまっているが、今更 C++ で拡張メソッドである。func(a, x) のようにメンバ関数外のものが a.func(x) で呼べるというやつである。まぁ、「今更」というくらいであって既にやってる方は色々いるわけだが。@cpp_akira (faith_and_brave) さん (2008年)じくよろさん(でいいのだろうか?)(2009年)@gim_kondo さん(2011年)などが作られているわけである。

もちろん C++ 自体には拡張メソッドは存在しないのでそれっぽいものを実現する仕組みを自前で作ることになるのだが、基本的に C++ で拡張メソッド風なものを実装しようとした場合、拡張するメソッド(関数)以外に大体 3 つの構成要素が必要だと思われる。

  1. 対象のオブジェクト、あるいは、引数を保持するオブジェクト(以下ラッパと表記)
  2. 上記ラッパを作成、値を紐付けする仕組み(以下ラッパ束縛と表記)
  3. 紐付けされなかった方と結びつけて実際の呼び出しを行う仕組み(以下ラッパ呼び出しと表記)

これで先の方の実装について整理するとこんな感じだろうか。じくよろさんの実装は最終的にフリー関数へ転送しているが「func(a, x) を」という形式に拘らなければ関数オブジェクト内でそのまま拡張メソッドの内容を実装してしまえばいいのでそのように実装した場合として記述している。

実装ラッパラッパ束縛ラッパ呼び出し
@cpp_akira さん拡張メソッドの内容を表す関数オブジェクト内に引数も保持コンストラクタoperator| のオーバーロード(任意の1引数関数オブジェクトを受ける)
じくよろさん拡張メソッドの内容を表す関数オブジェクト内に引数も保持コンストラクタoperator, のオーバーロード(関数オブジェクト限定)
@gim_kondo さん拡張メソッドの内容を表す関数オブジェクト内に対象オブジェクトへの this ポインタを保持対象オブジェクトへのメンバテンプレート埋め込みとマクロ置換によるコンストラクタ呼び出し関数オブジェクトの呼び出し

@gim_kondo さんの実装は . (ドット演算子)で呼び出せるようになっているが拡張メソッド呼び出し側でのマクロ置換発生は代償としてちょっと evil だと思われる。この判断をした時点で基本的に演算子オーバーロードで実装ということになるのだが、今回選んだ演算子は ->* である。ほとんどの C++er は使ったことがないだろうと思われる、というかひょっとしたら存在を知ってる人すら少ないかもしれない。通常の使い方は以下のような形である。

struct A { void func(void) {} };

int main(void)
{
 void (A::*mp)(void) = &A::func;
 A a, *pa = &a;
 (pa->*mp)();
 return 0;
}

つまりメンバないしメンバ関数へのポインタを参照するためのものである。とりあえず括弧の付け方に注意。知らないとまず間違えると思う。とりあえずこれがなくて困るとは普通ならない(そもそも普通の使い方ならポインタに対して使う)し、メンバアクセスのための演算子なので拡張メソッドとしては意味は近いはず、ということから選定。

で、拡張メソッド的なものが作れるというのは分かっている上でなぜまた(特に需要もないのに)別に作るのか。↑で書いたとおり、C++ で拡張メソッドを作ろうとすると本来の拡張メソッドだけでなく他の仕組みも必要になるわけでそれが面倒い。できるだけ拡張メソッド本体以外の余計な部分は勝手に作成させたい。で作ってみたのがこんな感じ。内部実装は https://github.com/yak1ex/cpp_stuff/blob/master/extender.hpp にある。

#include <iostream>
#include "extender.hpp"

namespace test { struct A { int n; }; }

namespace ext {

DEFINE_EXTENDER1(test::A, func1, {
 typedef test::A& result_type; // MUST follow result_of protocol

 result_type operator()(test::A& a, int &n) const {
  std::cout << "int&" << std::endl;
  return a;
 }
 result_type operator()(test::A& a, const int &n) const {
  std::cout << "const int&" << std::endl;
  return a;
 }
});

DEFINE_EXTENDER2(test::A, func2, {
 typedef test::A& result_type; // MUST follow result_of protocol

 result_type operator()(test::A& a, int &n) const {
  std::cout << "int&" << std::endl;
  return a;
 }
 result_type operator()(test::A& a, const int &n) const {
  std::cout << "const int&" << std::endl;
  return a;
 }
});

}

int main(void) {
 using ext::func1;
 using ext::func2;

 test::A a = { 0 }; int n = 0;

 ((a->*func1)(1)->*func1)(n); // Cascading but unintuitive

// NOTE: different from ordinary operator semantics/precedence
 a->*func2(1)->*func2(n);

 return 0;
}

実行結果

const int&
int&
const int&
int&

g++ 4.[5678] それぞれで -std=c++0x 有無両方で動作を確認している(いくつか workaround も入っている)。通常の演算子の優先順位の意味論に従ったのが EXTENDER1 の方、使いやすさを優先したのが EXTENDER2。まぁ普通は EXTENDER2 の方だと思う。使う側としてはほぼ書きたい拡張メソッドの内容部分のみだけで実現できている、と言っていいと思う。マクロにしてあるが↓なので直書きでもそんなに変わらない。なお、上記の例では 1 引数同士で const の違いだけでオーバーロードしているが、任意の型で引数の数が違っている場合でもそのまま operator() を書けばオーバーロード可能である。

#define DEFINE_EXTENDER1(target, name, ...) \
struct BOOST_PP_CAT(name, _functor) : public yak::util::extender1<BOOST_PP_CAT(name, _functor), target> \
__VA_ARGS__ name
#define DEFINE_EXTENDER2(target, name, ...) \
struct BOOST_PP_CAT(name, _functor) : public yak::util::extender2<BOOST_PP_CAT(name, _functor), target> \
{ \
 struct _ __VA_ARGS__; \
} name

内部実装について簡単に説明すると、CRTP + Barton Nackman trick を使ってラッパと演算子を定義している形。上表と同じ形で書くと次のようになる。

実装ラッパラッパ束縛ラッパ呼び出し
EXTENDER1拡張メソッドの内容を表す関数オブジェクト内に対象オブジェクトも保持operator->* のオーバーロードで関数オブジェクトを返す関数オブジェクトの呼び出し
EXTENDER2引数を保持するオブジェクトを関数オブジェクトと別に用意operator() のオーバーロードでラッパを返すoperator->* のオーバーロード(ラッパ限定)

0 件のコメント:

コメントを投稿