Curiously Recurring Template Pattern (CRTP) と Policy-based design
この記事はEEIC Advent Calender 2016の12月22日分として書かれた.
CRTPとは
C++でのジェネリックプログラミングで広く知られたパターンとして, Curiously Recurring Template Pattern (CRTP) というものがある. この名前は実態から大分かけ離れているので, CRTPが何たるかを要約しておくと次のようになる. 「基底クラスに派生クラスの型を与えると, 基底クラスから派生クラスを触ることができる.」 基本的にはそれだけのことである. それだけのことなのだが, なかなかに興味深い性質を持っているのでわざわざ名前が付いている.
template <typename Derived> class base { // ... }; class derived : public base<derived> { // ... };
CRTPという名前に「なってしまった」理由は, 1995年にJames O. Coplienという人物が同名の論文として発表したことによる. Coplien自身はCRTPのテクニックを発明したわけではなく, 複数のプログラマが独自に再発明しているのを見て これがある種のパターンではないかと気付いた,ということらしい. わざわざ「私が名付けるのは避けたい」とまで言っていることから, まさかこんな名前で広まるとは思っていなかったのだろう.
Curiously recurring template pattern - Wikipedia
ジェネリックプログラミングを実践しているプログラマの多くは, CRTPが何となく便利だとは認識していても, 「なぜ便利なのか」を認識していないかもしれない. Coplienの論文には,「何のためにCRTPを使うのか」が一言で明確に示されている.
That is, we encode the circular dependency directly using inheritance in one direction and templates in the other.
C++では継承によってクラス間の依存関係を表すことができるが, それは単方向の依存関係に限定される. 双方向な依存関係,すなわち循環依存をクラス間で表現する手法がCRTPであり, 片方を継承,もう片方をテンプレート引数として表現することで, 循環依存が発生しているクラスをコンポーネントに分解することが可能になる, というわけである.
「CRTPは静的多態性をうんたら」とかそういう論調のサイトをよく見かけるのだが, 12年前にCryolite先生が書いているように, 私もその説明には今ひとつ納得がいっていない. どちらかというと,CRTPはもっと一般的な手法で, 多態性の表現として使うのは応用の一種に過ぎない,という風に思っている. 循環依存が生じるパターンだったらおよそ何でも役に立つはずなのだ. 一応,オブジェクト指向の理論的枠組みでは CRTPのことを"F-Bounded Polymorphism"と呼ぶらしいが, 元論文がパッと見た感じ意味不明だったので誰か解読したら教えて欲しい.
細かい理屈は置いておいて,実用例を見ると理解が早くてよい.
CRTPを活用した有名なライブラリとして,
Boostのiterator_facade
というテンプレートクラスを見てみる.
template < class Derived , class Value , class CategoryOrTraversal , class Reference = Value& , class Difference = ptrdiff_t > class iterator_facade;
iterator_facade
は,自作イテレータを実装する際に要求される
各種関数を自動生成してくれるライブラリである.
イテレータを定義するには,
似たような演算子を大量に定義する必要があって,
そういう演算子を一つ一つ手で実装するのは時間の無駄である.
しかし,だからといってイテレータという抽象化がダメだというわけではない.
イテレータとして本来必要な機能はせいぜい「移動する」「デリファレンス」「比較する」ぐらいしかないので,
それらの実装だけはコンテナごとに別々にしてインターフェイス部分を共通化することでイテレータを簡単に作れるようになるはずだ,
というアイディアに基づくのがiterator_facade
である.
class int_iterator : public boost::iterator_facade< int_iterator, int, std::output_iterator_tag, int > { public: explicit int_iterator(int x) noexcept : x_{x} { } private: friend boost::iterator_core_access; // 最低限の機能だけ実装する void increment() noexcept { ++this->x_; } int dereference() const noexcept { return this->x_; } bool equal(const int_iterator& other) const noexcept { return this->x_ == other.x_; } int x_; }; int main() { std::for_each(int_iterator{1}, int_iterator{5}, [] (int x) { std::cout << x << " "; }); // Output: 1 2 3 4 }
iterator_facade
のドキュメント
には,「なぜポリシーオブジェクトではなくCRTPが選択されたのか」について書いてある.
ポリシーオブジェクトがなんなのかがこの文書だけでは釈然としなかったが,
CRTP以外のやり方という文脈から察して,
おそらく継承関係を逆にしたりコンポジションにしたりしたラッパのことを指しているのだと思う.
template <typename Iterator, /*...*/> class iterator_facade : public Iterator { /*...*/ };
こういうラッパではなくあえてiterator_facade
でCRTPが使われている理由には,次の3つが挙げられている.
- ラッパの生成やコピーにオーバーヘッドがかかる.
- C++の規格上,空の基底クラスにはEmpty Base Optimization (EBO) が効いて,空間コストが必ず除去されることが保証される.
- 現実的には,メンバを追加しない空の派生クラスでもオブジェクトのレイアウトが変わらない状況がほとんどな気はする.
- 利用者がコンストラクタをカスタマイズできない.
- これはそこそこ本質的な問題であろう.C++11だとInheriting Constructorsでマシになるが,根本的には解決していない.
- パラメタライズされたイテレータ(例えば
foo_iterator<T>
のようにT
でパラメタライズされたもの)を作る時, (Template AliasesのないC++03以前では)foo_iterator_generator<T>::type
のように型生成テンプレートを経由する必要がある.
そもそものところ,
新しいイテレータを作る時にfoo_iterator
ではなくて
iterator_facade<...>
という型になり,
ユーザに内部の実装詳細であるはずのiterator_facade
が見えているということがおかしい.
あくまで「イテレータのインターフェイスを自動実装するクラス」を利用した「新しいイテレータクラス」を提供するわけで,
ラッパというやり方は自然な依存関係の表現とは言い難いわけだ.
しかし,実際のところインターフェイスを定義するために
最終的な派生クラスの型が要るから,それをテンプレート引数として
注入するのがCRTPの役割である,といえる.
興味深いこととして,
CRTPには実行時のオーバーヘッドが全くない(ゼロオーバーヘッド抽象化)ので,
iterator_facade
を使うかどうかについて性能面で悩む必要が全くない.
逆に言えば,CRTPを使う動機には「性能が絶対要件である」という前提が欠かせない.
性能がどうでもよいのなら,実際のところ仮想関数で何とかなる場合が多いからだ.
こういった考え方こそC++らしいもので,
それ故にCRTPはC++のライブラリ設計で頻出の手法になっている.
ちなみに,iterator_facade
も現在C++標準に提案されているらしい(採用されたわけではない).
Ranges TSを精力的に開発しているEric Nieblerが著者の一人のようである.
Iterator Facade Library Proposal for Ranges
テンプレート引数地獄
必ずしもCRTPに限らないが,
テンプレートをフル活用したプログラムにありがちな問題として,
「テンプレート引数の数が増えまくる」というものがある.
再びiterator_facade
を例に考えると,
後ろ2個のテンプレート引数にはデフォルト値があるので,
利用者からは最低限3個の引数を指定すればよい,ということになっているが,
テンプレート引数が5個であることに変わりはない.
このことは,
ライブラリを利用する立場だとコンパイル時・リンク時・デバッグ時に表面化する.
例えば,上のコードでoperator++
の実体は
boost::iterators::detail::iterator_facade_base<int_iterator, int, std::__1::output_iterator_tag, int, long, false, false>::operator++()
という名前である.
int_iterator
の利用者からみて,
long
だのfalse
だのの情報は明らかにいらないし,
デバッグ中に見たい情報ではない.
残念ながらこれでもまだ単純すぎる例で,
さらに少し複雑なコードになると画面が埋め尽くされるほどの型情報が
コマンドを叩く度にわらわらと出て来る.
特にデバッグ中に見たいのは型情報よりも変数の値であって,
そういう冗長な型情報は端的にいってデバッグの邪魔である.
ライブラリを開発する側もつらい.
例えば,iterator_facade
のコードを少し引用すると,こんな感じのがあちらこちらにある.
# define BOOST_ITERATOR_FACADE_INTEROP_HEAD_IMPL(prefix, op, result_type, enabler) \ template < \ class Derived1, class V1, class TC1, class Reference1, class Difference1 \ , class Derived2, class V2, class TC2, class Reference2, class Difference2 \ > \ prefix typename mpl::apply2<result_type,Derived1,Derived2>::type \ operator op( \ iterator_facade<Derived1, V1, TC1, Reference1, Difference1> const& lhs \ , iterator_facade<Derived2, V2, TC2, Reference2, Difference2> const& rhs)
Boostのコードはプリプロセッサの闇魔術で色々圧縮されているものの,
「テンプレート引数が多すぎる」という本質的な問題は
残されたままである.
例えば,iterator_facade
のテンプレート引数にもう一つ新規に追加するとしたら?……
ヘッダ中のあちらこちらを直さなければいけないだろうし,
苦痛以外の何者でもない.
テンプレート引数の羅列は順序に依存している
ということも問題である.
例えばiterator_facade
に与えるテンプレート引数のどれかを交換すれば,
おびただしい数のコンパイルエラーになること間違い無しである.
『C++の設計と進化』には,C++の設計過程で順序依存をなくす工夫が
なされていることがあちこちに書いてある.
順序依存は可能な限り避けるべきであり,
そして推奨されるプログラミングパターンに含まれているべきでもない.
循環参照との戦い
そもそもなぜValue
だのCategoryOrTraversal
だのといった型を渡さなければならないのか?
どうせDerived
を渡しているのだから,Derived
の中で型を定義してしまえば,
基底クラスはそのメンバを参照できるはずじゃないか,
と思うのは自然な発想である.
template <typename Derived> class iterator_facade { public: Derived& operator += (typename Derived::difference_type n); // エラー // ... };
しかし,残念ながら,C++の言語仕様上このコードはコンパイルできない.
Derived
がincomplete typeとかそういうエラーが出るだろう.
なぜかというと,派生クラスのメンバにアクセスするためには,
基底クラスのメンバの型やシグネチャ*1が確定していないといけないからである.
すなわち,基底クラスのメンバ関数のシグネチャが
派生クラスのメンバに依存するようなコードは,そもそも書けない.
そのため,CRTPが活躍できるのは,あくまで
「基底クラス内のメンバ関数の"実装"が派生クラスに依存している」
という状況に限られている.
よく知られた回避策は2つある.
- 基底クラスのテンプレート引数を増やす.
- メンバテンプレートや戻り値型推論を活用することで,型の依存関係を逆向きに変える.
1番目の解決策は先ほどまで述べた通りである. 2番目の解決策だと,例えばこんな感じになる.
template <typename Derived> class iterator_facade { public: template <typename Difference> Derived& operator += (Difference n); // ... };
この場合だとなんとかうまくいくのだが, 一般にはこれでは対処しきれない場合が存在する. 最も単純な例としては,
template <typename Derived> class foo { typename Derived::value_type val_; };
というように,基底クラス内で依存型を実体化しなければならない場合に起きる. こういうコードは普通にあり得るので, 2番目の解決策は即座に限界を迎える. また,そもそもメンバテンプレートでなかった単純なメンバ関数が, 依存性解決のためだけにテンプレートになって複雑性が増大しているのも問題である. こうしてテンプレート引数が増えると, 同じようにデバッグ時などに困るだけである.
こういう状況に何度も直面した後,私は次のような考えに至った.
シグネチャを確定させるための型情報は,
CRTPを積極的に活用する上でやむを得ず必要になるので,
それらは事前に与える以外の解決策がない.
そして,そのための最も単純で使いやすい手法は,
「CRTPに使うためのDerived
もまとめて」丸ごと一つのテンプレート引数として与える,
というものである.
template <typename P> class iterator_facade { using derived_type = typename P::derived_type; using value_type = typename P::value_type; using category_type = typename P::category_type; using reference_type = typename P::reference_type; using difference_type = typename P::difference_type; public: // ... };
先ほどまでDerived
というテンプレート引数を与えていた代わりに,
派生クラス自身ではない型にderived_type
というメンバを持たせることで,
テンプレート引数に陽に現れない形で間接的にCRTPを実現する.
そして,シグネチャを確定させるための情報も一緒にその型に持たせてしまう.
たったこれだけの変更だけで,
テンプレート引数が増大する問題を回避しながら,
CRTPも活用できるようになるのである.
ところで,P
とは何だろうか?
これはまさしく次に解説する ポリシー (Policy) そのものであるといえる.
CRTPとPolicy-based design
ポリシーを使った設計それ自体は,何ら新しいことではない. 2001年に出版された『Modern C++ Design』(略称MC++D) *2 という本には, ポリシーを使ってスマートポインタを構成するといった, 当時としては相当先進的なジェネリックプログラミングのスタイルが示されている. しかし,そうはいっても,MC++Dが出版されて既に15年以上経っているわけで, ポリシーが新規の概念かというとそういうことはない.
template < typename T, template <class> class OwnershipPolicy, class CoversionPolicy, template <class> class CheckingPolicy, template <class> class StoragePolicy > class SmartPtr;
このスマートポインタは,
ポリシーを切り替えるだけで参照カウント方式や
暗黙変換の方式などを変更することができる.
様々なスマートポインタに共通の処理だけがSmartPtr
に実装されていて,
例えばアトミック変数で参照カウントを制御するといった詳細は
ポリシーに分離されている.
MC++Dで解説された「ポリシー」と
この記事で述べている「ポリシー」が
一つ決定的に違っているのは,ポリシーとして
テンプレートクラスではなくただのクラスを用いていることである
*3.
テンプレートクラスを受け取る形式は,
上の例だと利用者がT
を指定する必要が無いという利点があるが,
しかしテンプレート引数が増える問題は解消できていない.
実際のところ,利用者は既にT
の実体を知っているわけだから,
ポリシーとして与える際にT
を知っている前提で型を定義することに
それほど手間がかかるわけではないのだ.
PolicyとTraits
C++11の標準ライブラリにType Traits (型特性)があるので, Traitsは比較的知られている概念だと思う. 型同士の相関を考えるという性質上, TraitsとPolicyはよく似ているので紛らわしいが, 違いについて下のように解説がある.
What is the difference between a trait and a policy?
Traitsとは要するに「既存の型から情報を引き出す」操作,
Policyは逆に「情報をクラスに入れ込む」操作であるといえる.
この解説の例ではstd::allocator<T>
がPolicyの例として挙げられている.
アロケータはコンテナに「入れ込む」形になっているわけで,
よくよく考えてみるとSTLの中にもポリシーは使われている.
ポリシーの合成,入れ子
ポリシーを型として実装した場合, 直交したポリシーを合成するのは非常に簡単で, 多重継承を使えばよい. 以下の例だと,アロケーションとロガーを別々のポリシーとして定義し, それらを一つにまとめている.
template <typename Policy> struct my_list_base { using derived_type = typename Policy::derived_type; using value_type = typename Policy::value_type; struct elem { elem* next; value_type val; }; public: void add(const value_type& val) { const auto ptr = static_cast<elem*>( Policy::allocate(sizeof(elem)) ); ptr->next = head_; ptr->val = val; head_ = ptr; Policy::add_log("added element"); } void traverse() { for (auto cur = head_; cur != nullptr; cur = cur->next) { this->derived().on_traverse(cur->val); Policy::add_log("traversing..."); } } protected: my_list_base() : head_{} {} private: derived_type& derived() noexcept { return static_cast<derived_type&>(*this); } elem* head_; }; struct my_allocation_policy { static void* allocate(std::size_t size); static void deallocate(void*); }; struct my_logging_policy { static void add_log(const char*); }; template <typename T> class my_list; template <typename T> struct my_list_policy : my_allocation_policy , my_logging_policy { using derived_type = my_list<T>; using value_type = T; }; template <typename T> class my_list : public my_list_base<my_list_policy<T>> { private: friend my_list_base<my_list_policy<T>>; void on_traverse(T val) { std::cout << ++count_ << " " << val << std::endl; } std::size_t count_ = 0; };
このパターンをより複雑なものに一般化した場合,
ポリシーを前提に階層的なクラス設計を行う際に,
基底クラスに近づくにつれて「ポリシーが大きくなっていくような」ものになる.
例として,侵入型参照カウンタintrusive_ptr
の実装について考えてみる.
template <typename T> class my_intrusive_ptr { // ... }; template <typename Policy> class ref_counted_base { private: using derived_type = typename Policy::derived_type; using ref_count_type = typename Policy::ref_count_type; friend my_intrusive_ptr<derived_type>; void acquire_ref() { Policy::increment(ref_count_); } void release_ref() { if (Policy::decrement(ref_count_) == 1) { delete &derived(); // thisをdeleteするアグレッシブさ } } derived_type& derived() noexcept { return static_cast<derived_type&>(*this); } ref_count_type ref_count_{1}; }; template <typename Policy> struct atomic_ref_counted_policy : Policy { using ref_count_type = std::atomic<std::size_t>; static void increment(ref_count_type& count) { count.fetch_add(1, std::memory_order_seq_cst); } static std::size_t decrement(ref_count_type& count) { return count.fetch_sub(1, std::memory_order_seq_cst); } }; template <typename Policy> class atomic_ref_counted_base : public ref_counted_base<atomic_ref_counted_policy<Policy>> { // ... }; class my_class; struct my_class_policy { using derived_type = my_class; }; class my_class : public atomic_ref_counted_base<my_class_policy> { // ... };
この例だと,継承の真ん中にある
atomic_ref_counted_base
は,元あるポリシーを拡張して,
ref_count_type
といった新しいメンバを導入している.
大したコード量でないのでありがたみが分かりにくいが,
アトミック変数という実装詳細を後回しにして,
ref_counted_base
には「侵入型参照カウントを実装するとしたらこうなる」というコード片を
CRTPを駆使して括り出せている,ということが分かる.
そして,atomic変数を使うことを確定させた時,
実装すべきはincrement
といったより細分化された部品だけになっている.
循環依存の関係にあるわけだから, 継承が一段深くなる度にポリシーは逆方向に継承されていくのは自然であるといえよう. あるいは,継承関係が逆転することは, 「制御の反転」 の一形態と見ることもできる. ただし,あまりこういう複雑な継承関係を組み合わせすぎると意味不明になるので, やりすぎない程度にすることも大事である.
ここまでを俯瞰してみると, ポリシーというのは「そのクラス専用の名前空間のようなものを定義している」 と考えることもできる. 言い換えると,クラスが依存するコンテキストを限定することができ, 依存関係を最小化できる. そして,C++のクラスあるいは型というのは, ほとんど何でもできると言って良いぐらい色んな物を持てるから, 受け取る型が一つでも表現能力の面で困ることがない.
- 普通の関数 (staticメンバ関数)
- 普通の値(constexpr or 変数)
- 変数メンバ
- メンバ関数
- 型(typedef または 入れ子クラス)
- テンプレートクラス(メンバテンプレートクラス または テンプレートエイリアス)
そんなわけで,型を一つポンと渡すことでポリシーを表現するやり方は, シンプルな割に表現力も問題がないので便利だと思う.
CRTP共通基底クラス
CRTPでは大抵の場合,derived()
メンバを定義するのだが,
ポリシーが必ずderived_type
という型を持つとしたら,
この作業も共通化してしまえる.
まあ実際のところ無くても大して問題ではないが,コードの重複は少し減らせる.
template <typename Policy> class crtp_base { protected: using derived_type = typename Policy::derived_type; derived_type& derived() noexcept { return static_cast<derived_type&>(*this); } const derived_type& derived() const noexcept { return static_cast<const derived_type&>(*this); } };
動的なダウンキャストについては,
Boostにはpolymorphic_downcast
という小粒なライブラリと同じ手法を使えばチェックできる.
その動きを簡単に説明すると,デバッグ時にはdynamic_cast
を使って,
リリース時にはstatic_cast
を使う,というものである.
但し,ポリモーフィックでない型(=仮想関数が無いクラス)の場合は
コンパイル時に怒られるので,仮想デストラクタを強制的に付けてRTTIが動くようにする必要がある.
#ifdef ENABLE_POLYMORPHIC_CAST virtual ~crtp_base() = default; #endif derived_type& derived() noexcept { #ifdef ENABLE_POLYMORPHIC_CAST auto p = dynamic_cast<derived_type*>(this); if (!p) { throw std::bad_cast{}; } return *p; #else return static_cast<derived_type&>(*this); #endif }
例えばこんなミスを自動検出できる.
template <typename Policy> struct printer_base : crtp_base<Policy> { void print() { std::cout << this->derived().x << std::endl; } }; struct int_wrapper; struct double_wrapper; struct int_wrapper_policy { using derived_type = double_wrapper; // Broken }; struct int_wrapper : printer_base<int_wrapper_policy> { int x = 123; }; struct double_wrapper_policy { using derived_type = double_wrapper; }; struct double_wrapper : printer_base<int_wrapper_policy> { // Broken, too double x = 123.456; }; void f() { int_wrapper w; w.print(); // int_wrapper -> printer_base<int_wrapper_policy> -> double_wrapper }
デバッグ時とリリース時でオブジェクトのレイアウトが変わってしまうのは深刻な問題なので,
普遍的に役立つかというと微妙ではある.
上で示しているように必要なときだけ#ifdef
で有効にするか,
ポリモーフィックな型だけをメタプログラミングで判定して
チェックしたりといった使い方になるだろうか.
ポリシーでプロトタイピング
ここまでを踏まえて,最後により汎用的なプログラミングスタイルについて述べる. ポリシーの利用用途は上のような特殊なライブラリに限られたものではなく, ほとんど何にでも使える. ポリシーを使うと,C++のような静的型付け言語であっても トップダウンに素早くプロトタイピングすることが可能である.
ものすごく単純な例だが, 入力が少しずつ入ってくるタイプのtokenizer (カンマなどで文字列を切断するやつ)について考えてみる. まず何も考えずにクラスの名前を決める.
template <typename Policy> class tokenizer_base : public crtp_base<Policy> { // ??? };
まず文字を受け取る関数が要るだろうから,
add
みたいな関数を追加する.
となると,引数には文字を受け取るだろうから,
character_type
みたいな型をポリシーに要求する.
template <typename Policy> class tokenizer_base : public crtp_base<Policy> { using character_type = typename Policy::character_type; public: void add(character_type ch) { // ??? } };
新しい文字が来たら,デリミタ(切り分ける文字)かそうでないかのどっちかで, デリミタじゃなかったら単にバッファに溜めるみたいな処理になるはずである. デリミタかどうかの判断は派生クラスに丸投げして,バッファの型も追加する.
template <typename Policy> class tokenizer_base : public crtp_base<Policy> { using character_type = typename Policy::character_type; using token_buffer_type = typename Policy::token_buffer_type; public: void add(character_type ch) { auto& self = this->derived(); if (self.is_delimiter(ch)) { // ??? } else { // ??? } } private: token_buffer_type buf_; };
バッファは「文字を追加できる何か」だから
とりあえずvector
みたいなやつと考えることにして,
トークンができた後の処理は派生クラスに丸投げすることにする.
あとバッファがたまったままだと困るので,finish
みたいなやつも加える.
template <typename Policy> class tokenizer_base : public crtp_base<Policy> { using character_type = typename Policy::character_type; using token_buffer_type = typename Policy::token_buffer_type; public: void add(character_type ch) { auto& self = this->derived(); if (self.is_delimiter(ch)) { self.on_new_token(buf_); buf_.clear(); } else { buf_.push_back(ch); } } void finish() { this->derived().on_new_token(buf_); } private: token_buffer_type buf_; };
ポイントは,「ここでコンパイルにかけてしまう」ことである.
まだvector
を使うとか何も言っていないわけだが,
プロトタイピングをしている時は最終的な型がどうなるのか分からないので,
型が確定していないのも当たり前だ.
しかし,テンプレートのコードだから型を確定させなくてもこれだけでコンパイルはできる.
C++はコンパイルエラー地獄でよく知られている言語であるので,
C++プログラマはコンパイラと仲良くなる方法を知るべきであり,
その一つがシンタクスエラーなど単純なエラーを早い段階で潰しておくことである.
このクラスを実際に使うとなったら,実際にポリシーの実体と派生クラスを作る. この時初めてテンプレートが実体化されて, シンタクスよりも具体的なエラーが出てくるようになる.
class char_tokenizer; struct char_tokenizer_policy { using derived_type = char_tokenizer; using character_type = char; using token_buffer_type = std::vector<char>; }; class char_tokenizer : public tokenizer_base<char_tokenizer_policy> { public: void show_all() { for (const auto& token : tokens_) { std::cout << token << std::endl; } } void set_delimiter(char delim) { delim_ = delim; } private: friend tokenizer_base<char_tokenizer_policy>; bool is_delimiter(char c) { return c == delim_; } void on_new_token(const std::vector<char>& v) { tokens_.emplace_back(std::begin(v), std::end(v)); } char delim_ = ','; std::vector<std::string> tokens_; };
このような順番で考えていくと,
自然とtokenizer_base
から実装詳細が抜き取られ,
逐次的にトークン処理をする際に最低限必要なものだけが書かれていることが分かる.
必要に応じて基底クラスと派生クラスの仕様をすり合わせていくと,
分離されたモジュールとしていい具合に整備されてくるはずである.
そして何より大事なのが,こういう抽象化はオーバーヘッドなしに行える,
ということである.
まとめ
- CRTPはモジュール間の循環依存問題を解消する.
- テンプレート引数が増えるテクニックは望ましくない.
- CRTPとポリシーを組み合わせるパターンは極めて有用である.
- テンプレートのコンパイルエラーに負けない強い心を持つ.
明日のEEIC Advent Calendarは@nobi_sannです.
Uniform initialization syntaxの括弧問題についてのぼやき
C++11で導入されたUniform initialization syntaxは、C++の変数宣言にまつわる諸問題
(組み込み型とクラスの違い、Most Vexing Parse、narrow conversion)を解決する機能である。
しかし、Effective Modern C++でも取り上げられているように、
波括弧 {}だけではstd::vector
のようなクラスで呼び出せないコンストラクタが存在するという新たな問題が発生している。
std::vector<int> a(3, 123); // 3要素のベクタ: {123, 123, 123} std::vector<int> b{3, 123}; // 2要素のベクタ: {3, 123} std::vector<int> c{{3, 123}}; // 2要素のベクタ: {3, 123}
原因は周知の通り、std::vector
が「整数引数2個を受け取るコンストラクタ」と
「std::initializer_list
を受け取るコンストラクタ」の両方を持っているが、
オーバーロード解決時にUniform initialization syntaxはinitializer_list
をやたらと好むので、
前者のコンストラクタに一致させられないというものである。
特にテンプレート内の変数宣言では構築する型が分からない場合もあるので、
どちらを使うべきかという悩ましい問題になる。
どうもこの問題は言語設計上のミスだったのではないかという疑いを持ってしまう。 どう考えても普通のプログラムで一番よく使うのは普通のコンストラクタの呼び出しで、 それらに比べればstd::initializer_listを使うことは比較的稀だ。 丸括弧 ()を置き換えるためにUniform initialization syntaxを使うと、 我々はそこで暗黙のうちに「できるだけinitializer_listにマッチしよう」と主張してしまうことになる。 私の記憶では、当初Uniform initialization syntaxはあくまでInitializer listsの提案のおまけのようなものだったから、 こういう仕様になってしまった流れになった理由は何となく推察はできる。
ちなみに上の例だとb
もc
も同じ意味になる。
b
は他のコンストラクタよりも優先してinitializer_list
を探し、
c
はbraced-init-listを1つ取るコンストラクタを探し、
結果的に同じものにマッチすることになる。
私が思うに、b
のような配列の初期化を認めず、
常にc
のような形でinitializer_list
を使うことを強制すればよかったのではないかという気がする。
配列を初期化するときは常に波括弧を2つ使うことになるが、
配列の初期化なんてそう頻繁に表れるわけではないのだから2文字増えるぐらいよいではないか、と思う。
まあでも言語機能として導入されてしまった以上、それを考慮してコードにするしかない、 という結論しか出ないのだが・・・。
追記
数年前に議論されていたらしい.
vector/arrayとUniform initialization+Initializer list - yohhoyの日記
Non-type template parametersの威力
数年ぶりにC++関連のまとまった記事を書こうという気になった。 最近の私はというと研究で毎日C++を書いていたので記事にできそうなネタはたくさんあるのだが、 それを公開するのは若干面倒なので出し渋っていたところがある。 加えて、C++嫌いな人々の根も葉もない批判によって、 毎日強烈なストレスを受けてきたのも制約の一つだったかもしれない。 まあここで愚痴愚痴言う気もあまりないのだが、 プログラマが落ち着いて物事を考えるには周りの環境はとても大事ってわけだ。
この記事では、直近1年ぐらいで私が重要だと感じるようになった、 Non-type template parameters (型ではないテンプレート引数)を使ったイディオムをいくつか紹介しようと思う。 面白いことに、今回紹介する抽象化手法には実行時オーバーヘッドが全く無いため、 上手く組み合わせることで安全で生産的かつ高速なコードを記述することができる。
C++03までは、Non-type template parametersの重要な用途としてコンパイル時計算があったが、
そういった用途はC++11以降ではconstexpr
でほとんど置き換えられている。
今回紹介するのはコンパイル時計算とは異なった用途の例である。
アダプタ関数の生成
よくあるケースで、次のようなコールバック関数を要求するAPIがあるとしよう。
void set_callback(void (*func)(int, void*), void* data);
この例のfunc
は引数にユーザ定義のvoid*
を取ることができるようだ。
例えば、ユーザは以下のように任意の型を定義して、それに対応したコールバック関数を定義する。
// ユーザ定義型 struct my_data { int val; }; // コールバック関数 void my_handler(int info, void* ptr) { my_data* data = static_cast<my_data*>(ptr); // キャストが要る data->val = info; // イベントのデータを自分で用意した領域にコピー } my_data g_data; // コールバックに書き込んでもらうデータ void g() { set_callback(my_handler, &g_data); // コールバックを設定 }
上のコードには特に問題はない。
しかし、このset_callback
という関数はvoid*
を受け取っており、
タイプチェックが行われないので、以下のような危険なコードを簡単に書けてしまう。
double x; set_callback(my_handler, &x); // ptrはmy_data*型だったはずだが・・・
本題と少し逸れるが、static_cast
というのは(Cの"包容力の高い"キャストよりは大分マシとはいえ)
実際のところ大して安全ではないということも分かる。
T* -> (暗黙のキャスト) -> void* -> (static_cast) -> U*
というように2段階にキャストすると、
reinterpret_cast
相当のことが簡単にできてしまう。
(実際にこの罠に嵌っている人を見たことがある。)
T* t = /*...*/; void* v = t; // 暗黙のキャスト U* u = static_cast<U*>(v); // reinterpret_cast<U*>(t)と同じ
どうすればこのようなミスを防げるだろうか?
コールバックを登録する際には、関数の型とデータの型を比較できるはずだから、
本来はset_callback
の呼び出し時点で静的タイプチェックができるはずなのである。
やりたいこととしては、テンプレートを使ってこんなラッパー関数を書くことである。
// タイプセーフなラッパー関数 template <typename T> inline void set_callback_typesafe(void (*func)(int, T*), T* ptr) { set_callback(func, ptr); // ←タイプエラー } void my_handler(int info, my_data* ptr); void g() { set_callback(my_handler, &g_data); }
しかし、実際には矢印の場所でタイプエラーが起きる。
func
の型が、元の関数だとvoid (*)(int, void*)
だったのに、
ラッパー関数ではvoid (*)(int, my_data*)
になってしまっており、シグネチャが合っていない。
いい加減な解決方法としては、無理やりvoid (*)(int, void*)
にキャストしてしまうという方法がある。
template <typename T> inline void set_callback_typesafe(void (*func)(int, T*), T* ptr) { set_callback(reinterpret_cast<void (*)(int, void*)>(func), ptr); // 危うい }
型に無頓着なC言語プログラマがいかにも好きそうな方法だ。 ほとんど全てのアーキテクチャにおいて、ポインタはその指す先の型に関わらずただのアドレスだから、 実際に実行しても多分何もエラーは出ないだろう。 しかし、複雑なコードになればなるほど、こういうキャストをばらまいたコードは信頼出来ない品質になっていくだろう。
ランタイムの性能を犠牲にできるのなら解決は容易である。 例えばこんな風にする。
template <typename T> struct Temp { void (*func)(); T* ptr; }; template <typename T> void transfer(int data, void* ptr) { Temp<T>* temp = static_cast< Temp<T>* >(ptr); temp->func(temp->ptr); } template <typename T> inline void set_callback_typesafe(void (*func)(int, T*), T* ptr) { Temp<T> p = new Temp<T>; p->func = func; p->ptr = ptr; set_callback(&transfer<T>, p); }
このコードは安全だし、実際正しく動く保証がある。
しかし、実行時性能は(無理やりキャストする案と比べて)ひどく悪くなった。
func
は関数ポインタとして一時的に格納されるから、インライン化は事実上不可能になり、
2段階の関数コールが走る。
関数ポインタを一時的に格納するために動的メモリ確保が必要だし、
しかもポインタ2個分のメモリを余計に食っていることになる。
実際には機械語として出力されないはずのキャストを省略するためのコストとしては、あまりに大きすぎる。
ではどうするか。我々は無念にもvoid*
に屈し、キャストをばらまいた型安全でないコードを書くしかないのか?
いや、そんなことはない。
Non-type template parametersを使えば、次のようなラッパ関数を書くだけでよいのだ。
template <typename T, void (*Func)(int, T*)> void transfer(int data, void* ptr) { Func(data, static_cast<T*>(ptr)); } template <typename T, void (*Func)(int, T*)> inline void set_callback_typesafe(T* ptr) { void (*func)(int, void*) = &transfer<T, Func>; set_callback(func, ptr); }
そしてこれをこんな風に使う。
void my_handler(int, my_data*); my_data g_data; void g() { set_callback_typesafe<my_data, &my_handler>(&g_data); }
Func
はテンプレートパラメータだから、コンパイル時に決定される。
そのため、transfer
にはコンパイル時に関数が渡る。
set_callback_typesafe
の利用者は、
全くキャストを使わずに静的タイプチェックをしながら、
型のない世界にコールバックを渡せるのである。
関数をコンパイル時に渡すことで何が嬉しいかというと、 コンパイル時に呼び出す関数が決まるからコード中(要するにアセンブリ中のテキスト領域)に関数を埋め込めることである。 さらに、インライン化できる場合はそもそも関数呼び出しが消滅し、ラッパ関数のみが残る。 結果的に、インライン化が行われればオーバーヘッドがゼロ、ということが実現できる。 2個の関数呼び出しをコンパイル時に1個に融合させた、ともいえる。
「なんだ、キャストがなくなっただけじゃないか」と思う人もいるだろうが、 この手法は別にキャストに限った話ではなく、応用範囲は広い。 例えばこんな風に
// 指定したコールバックを10回呼び出す謎のアダプタ関数 template <typename T, void (*Func)(int, T*)> void transfer_10(int data, void* ptr) { for (int i = 0; i < 10; ++i) Func(data + i, static_cast<T*>(ptr)); } template <typename T, void (*Func)(int, T*)> void set_callback_10(T* ptr) { void (*func)(int, void*) = &transfer_10<T, Func>; set_callback(func, ptr); } void h() { set_callback_10<my_data, &my_handler>(&g_data); }
関数の前処理/後処理を入れたり、複数の関数を合成したり、呼び出し回数や条件を変えたりなど、 要するに関数をラップする色んな転送関数がオーバーヘッドなしに作れる。 この特長は、コールバック関数に典型的なパターンを共通化して、 ジェネリックかつ高速なライブラリを作る際に特に役に立つだろう。
インライン化の促進
コールバック関数のインライン化に重点をおいた話として、Cryolite先生が興味深いコードを載せていた。
http://d.hatena.ne.jp/Cryolite/01000831
STLのアルゴリズム等に述語として関数ポインタを渡した際に、 コンパイル時に呼び出す関数が決定されるにも関わらず、関数ポインタの指す関数のインライン化が行われないことがある。 最近のGCCやClangは賢いのでそのような問題はないが、古めのコンパイラを使った場合に、 関数ポインタが引き回された際の解析を途中で諦めてしまうのである。
そこで、関数ポインタをNon-type template parametersとした関数オブジェクトを作るという手法がある。 すると、関数ポインタが型にエンコードされるので、手で関数オブジェクトを書いた時と同様の状況となり、インライン化が促進される。
元の記事のコードは後方互換性のために若干複雑なので、C++11で簡略化したコードを載せておく。 C++03ではVariadic Templatesが使えないので引数を並べる必要があるが、依然として実装は至って単純である。
template <typename Sig, Sig Func> struct inline_function; template <typename R, typename... As, R (*Func)(As...)> struct inline_function<R (*)(As...), Func> { template <typename... Args> R operator () (Args&&... args) const { return Func(std::forward<Args>(args)...); } }; // マクロにせずにfを2度書かなくて済む方法があったら教えてほしい #define MAKE_INLINE_FUNCTION(f) (inline_function<decltype(f), (f)>())
ちなみに、今となっては過去のイディオムだが、
MAKE_INLINE_FUNCTION
は、decltypeのないC++03においても実装できる。
初めて見た時はなかなかマジカルな手法だと思った。
// C++03 template <typename Sig> struct deducer { template <Sig Func> inline_function<Sig, Func> make() { return inline_function<Sig, Func>(); } }; template <typename Sig> inline deducer<Sig> make_deducer(Sig /*ignored*/) { return deducer<Sig>(); // 実行時の関数ポインタを捨てるのがミソ } #define MAKE_INLINE_FUNCTION(f) (make_deducer(f).make<f>())
こうして定義したinline_function
は、こんな風に使う。
// 与えられた関数を10回呼び出すジェネリックな関数 template <typename Func> void f(Func func) { for (int i = 0; i < 10; ++i) func(i); } void g(int x) { std::cout << x << std::endl; } // ... f(&g); // 古いコンパイラだとインライン化されないかも f(MAKE_INLINE_FUNCTION(&g)); // ほぼインライン化される
近年のコンパイラは賢いので、このテクニックによるインライン化の促進自体はさほど重要ではないだろう。 しかし、依然として静的解析の弱いコンパイラは存在するので覚えておいて損はない。
なお、最初のコールバックを登録する話題の中で、利用側で毎回シグネチャを渡していたが、この話と混ぜると少しだけ短くできる。 関数を型にエンコードする部分は分離したほうが分かりやすい気がする。
template <typename T, void (*Func)(int, T*)> inline void set_callback_typesafe(inline_function<void (*)(int, T*), Func> /*ignored*/, T* ptr) { void (*func)(int, void*) = &transfer<T, Func>; set_callback(func, ptr); } void f(int, my_data*); my_data g_data; void g() { set_callback_typesafe(MAKE_INLINE_FUNCTION(&f), &g_data); }
グローバル変数へのポインタ
グローバル変数は、スコープを飛び越えてありとあらゆる関数に副作用をもたらす邪悪な存在である。 一方で、C++プログラマはしばしば忘れがちであるが、 構造体メンバにポインタ経由でアクセスするよりも、 グローバル変数に直接アクセスする方が高速になる場合が多い。 グローバル変数だと、構造体メンバのアドレスをコンパイル時に決定できるので、 ポインタの値をメモリからロードしたり、オフセットを足しこんだりする命令が不要になるためである。
単純化した例として、次のようにクラスfooをクラスbarに集約して、 barに転送関数を記述したコードを考える。
class foo { public: void set(int val) { val_ = val; } private: int val_; }; class bar { public: bar(foo* f) : f_(f) { } void set(int val) const { f_->set(val); } private: foo* f_; }; foo g_foo; bar g_bar(&g_foo); void set_value_1(int val) { g_foo.set(val); } void set_value_2(int val) { g_bar.set(val); }
これをClangでコンパイルすると、次のようなアセンブリが吐かれる。
__Z11set_value_1i: # ... movl %edi, _g_foo(%rip) # 直接g_foo.val_に代入 # ... __Z11set_value_2i: # ... movq _g_bar(%rip), %rax # g_bar.f_の値をロード movl %edi, (%rax) # 間接的にg_foo.val_に代入 # ...
g_bar.f_
の値は実行中に書き換わるかもしれないから、
コンパイラとしては必ずf_
を一度ロードして、
その結果得られたアドレスを使わなければいけない。
しかし、f_
の指す先が特定のグローバル変数だと決まっているのなら、
このような間接参照は無意味であるし、コンパイル時に取り除くべきである。
この例だと、g_bar.f_
が定数であることを明示すれば、
コンパイラは冗長なポインタのロードを除去できる。
const bar g_bar(&g_foo); // g_bar.f_をconstにした void set_value_2(int val) { g_bar.set(val); }
__Z11set_value_2i: # ... movl %edi, _g_foo(%rip) # ...
というわけでconst
をたくさん付けましょうで話が済めばいいのだが、実際のところそう簡単ではない。
少なくともClangでは、デストラクタを加えて少し複雑にしただけで、
再び不要なポインタのロードが発生してしまうのである。
class bar { public: bar(foo* f) : f_(f) { } // デストラクタを加える (実際のところ空でも同様) ~bar() { std::cout << "~bar()" << std::endl; } void set(int val) const { f_->set(val); } private: foo* f_; }; foo g_foo; const bar g_bar(&g_foo); void set_value_2(int val) { g_bar.set(val); // g_bar.f_をロードしてしまう }
デストラクタを加えたところで、f_
がコンパイル時に決定されることに変わりはないから、
コンパイラは依然としてg_bar.f_
のロードを取り除けるはずなのだが、そうはならない。
この現象については私も正確な理由が分からないのだが、可能性としては次のように考えられる。
プログラム終了時にグローバル変数のデストラクタを呼ぶためには、事前に__cxa_atexit
を使ってデストラクタを登録する必要がある。
デストラクタの登録作業もコンストラクタの仕事の一部と考えれば、
bar
は定数で初期化できないということになる。
しかし、この理屈だと、デストラクタが空でも同じ理由が説明できていない。
いずれにせよ、我々がコンパイラに伝えたいことは、 グローバル変数を指して初期化したので間接参照を省いてほしいということである。 クラスの利用側の状況によって、コンパイル時にポインタの値が決定できる場合もあるし、 実行時にしかわからない場合もありうるが、 コンパイル時に決定できるなら必ず間接参照を省いてほしいということになる。
一つの解決策として紹介したいのが、やはりNon-type template parametersを使った手法である。 まず、グローバル変数を表すテンプレートを作り、 それにポインタ的なインターフェイスを付ける。
template <typename T, T* Obj> struct global_object { T& operator * () const { return *Obj; } T* operator -> () const { return Obj; } };
そして、これの利用を意図したクラスを次のようにテンプレートで定義する。
template <typename Foo> class bar { public: bar(Foo f) : f_(f) { } ~bar() { std::cout << "~bar()" << std::endl; } void set(int val) const { f_->set(val); } private: Foo f_; };
テンプレート引数であるFoo
には、
グローバル変数を紐付けたい場合global_object
を入れ、
実行時にアドレスを変更したいならポインタ型を入れればよい。
// グローバル変数なら型として埋め込む const bar< global_object<foo, &g_foo> > g_bar( (global_object<foo, &g_foo>()) ); // Most vexing parseの一例になってしまっている void set_value_2(int val) { g_bar.set(val); } void set_to(foo* f) { bar<foo*> b(f); // 実行時に決定することもできる b.set(123); }
ポインタを持った全てのクラスをこのように定義するのは若干面倒だが、 グローバル変数を直接触るコードよりは遥かに柔軟性が高いし、 その一方でコンパイラの最適化能力に関わらずオーバーヘッドは確実にゼロにできるので、 手段の一つとしてありなのではないかと思う。
まとめ
Non-type template parametersを使うことで、関数や変数、定数などを型にエンコードし、 コンパイル時の色んな最適化を促進することができる。 性能要件の厳しい状況下において、 C++が備えているゼロオーバーヘッドの抽象化能力は特に役立つ。
Java向けにC++クラスをラップする方法
JNI (Java Native Interface)ネタは、この記事で一応最後です。
前々回は、Javaから渡された配列や文字列を簡単に受け取る方法について述べました。配列の受け渡しに関しては、最悪だと呼び出し毎にネイティブヒープへのコピーが発生する可能性があるので、ネイティブ側とVM側のどちらにデータを配置するのが正しいのかを考慮して設計する必要があります。
前回は、C++をJNI経由で呼び出す際の例外処理について述べました。STLに例外が必要となるわけですし、Android等でも基本的には-fexceptionsを有効にしてビルドした方がいいと思います。
さて、ここまでの知識だと、JavaからC言語のインターフェイス経由で関数を呼び出すということは出来ますが、C++のクラスをJavaから管理するとなるともう少し工夫が必要です。今回は、オブジェクト指向なやり方でC++のクラスを利用するにはどうすればいいのかについて述べていきます。
ポインタなんて所詮整数だ
Javaには、CやC++のポインタを認識する仕組みはありません。*1Java仮想マシン上の"綺麗な世界"には、ポインタなどという穢らわしきモノは存在しないわけです。
しかし、結局のところ、ポインタ≒アドレスなんて所詮整数なので、Javaのクラスに整数メンバを用意しておけばアドレスを保存しておくことができます。そいつをJNI経由でC++側から読み出せば、C++側でインスタンスを特定できるという話です。今回の記事の根幹はここだけです。
この辺りの話題は、以下の記事を参考にしています。
http://thebreakfastpost.com/2012/01/21/wrapping-a-c-library-with-jni-introduction/
例として、以下の様なテストクラスをC++によって実装してみることにします。
public class WrapperTest { private long mHandle = 0; // <=== こっそりアドレスを保存する public WrapperTest() { init(); } private native void init(); public native void put(int x); public native void show(); public native void destroy(); }
ほとんどネイティブ関数となっていますが、mHandleなる整数メンバを入れていることに注目です。*2こいつをC++から読み書きすることで、Javaオブジェクトにぶら下がったC++インスタンスのアドレスを特定します。
ちなみに、Android標準ライブラリの多くが裏でネイティブを呼び出す形になっているのですが、使われているテクニックはやはり同じのようです。
ラッパークラス用テンプレート
さて、ここからはC++での実装に入っていきます。理屈は極めて単純なものの、毎回新しいラッパークラスを作る度に整数<->ポインタ間キャストについて考えるのは面倒でしょう。というわけで、ラッパークラスを作るための補助テンプレートについて紹介します。参考記事よりはもう少し賢い実装になっていると思います。
ほぼ同様のコードはGitHubにも上げてあります: https://github.com/meryngii/jni_util
template <typename T> class Wrapper { public: explicit Wrapper(JNIEnv* env, jobject objj) : env_(env), objj_(objj) { } Wrapper(const Wrapper&) = default; Wrapper& operator = (const Wrapper&) = default; T& get() const { T* ptr = getPointer(); if (!ptr) { throw std::runtime_error("Uninitialized native object was used!"); } return *ptr; } template <typename... Args> void create(Args&&... args) const { destroy(); // delete if an object exists setPointer(new T(std::forward(args)...)); } void destroy() const { delete getPointer(); setPointer(nullptr); } // Pointer Operations T* getPointer() const { return reinterpret_cast<T*>(env_->GetLongField(objj_, getFieldID(objj_))); } void setPointer(T* ptr) const { env_->SetLongField(objj_, getFieldID(objj_), reinterpret_cast<jlong>(ptr)); } private: jfieldID getFieldID(jobject objj) const { if (!classj_) { classj_ = env_->GetObjectClass(objj_); checkException(env_); } if (!fieldID_) { fieldID_ = env_->GetFieldID(classj_, "mHandle", "J"); checkException(env_); } return fieldID_; } private: JNIEnv* env_; jobject objj_; static jclass classj_; static jfieldID fieldID_; };
ポイントとなる関数はgetFieldID, set/getPointerの3つで、後はオマケです。Javaのメンバ変数を読み書きするにはjfieldIDが必要となるので、ネイティブ側でstatic変数にキャッシュしておきます。
jclassやjfieldIDといった値は、実行時に一度手に入れば(確か)プロセス終了時まで有効なので、必ずキャッシュしましょう。ちなみに、jclassやjobjectはクラスやオブジェクトが同じでも異なるポインタ値となる可能性があるので、env->IsSameObjectといった関数がわざわざ定義されています。jfieldIDやjmethodIDは一意性が保たれているのでキーとして使うことも可能ですが、個人的にあまり危なそうなことはオススメしません。
上のWrapperテンプレートを使えば、後はラッパークラスを作るのは簡単です。C++で普通にクラスを作って、それにJNIのインターフェイスを加えます。
class WrapperTest { public: void put(int x) { data_.push_back(x); } void show() { for (int x : data_) std::cout << x << " "; std::cout << std::endl; } private: std::vector<int> data_; }; extern "C" { JNIEXPORT void JNICALL Java_WrapperTest_init (JNIEnv* env, jobject thisj) { convertException(env, [=] () { Wrapper<WrapperTest>(env, thisj).create(); }); } JNIEXPORT void JNICALL Java_WrapperTest_put (JNIEnv* env, jobject thisj, jint x) { convertException(env, [=] () { auto& self = Wrapper<WrapperTest>(env, thisj).get(); self.put(x); }); } // ... }
ネイティブオブジェクトの破棄
基本的な話としては以上ですが、まだオブジェクトの破棄について言及していませんでした。
JavaのオブジェクトはGCによって不定期に開放されます。finalizeメソッドをオーバーライドすることによって、GCが走った時にリソースの最終処理を行うことが可能になっているので、普通に考えればC++のオブジェクトもその際に開放するのが自然な発想でしょう。ところが、finalizeメソッドはいつ呼び出されるか予測が出来ず、しかもアプリケーションの終了タイミングによっては呼び出されない可能性があります。
C++プログラマは「デストラクタは必ず呼び出される」という考えでクラスを設計するので、finalizeにネイティブリソースの破棄を一任するのは良い考えとはいえないでしょう。結局のところ、少々面倒ではありますが素直に破棄用メソッドを作って、Java側のクラス利用者がfinallyなりで責任を持って呼び出すという手法が一番マシと考えられます。上のクラスにはdestroyメソッドを定義してあります。
extern "C" JNIEXPORT void JNICALL Java_WrapperTest_destroy (JNIEnv* env, jobject thisj, jstring strj) { convertException(env, [=] () { Wrapper<WrapperTest>(env, thisj).destroy(); }); }
finalizeは当てになりませんが、一応保険として最後に破棄するようにしておくことは悪くないと思います。そのためにもdestroyは何度呼び出してもOKな作りにしておく必要があるでしょう。
@Override protected void finalize() throws Throwable { destroy(); super.finalize(); }
おわりに
最後に、JNIを使う際の注意点について簡単にまとめておきます。
- Java Native Interface を使用する上でのベスト・プラクティスをじっくり読む。
- (上にも書いてあるが)JNIの返り値は全力でグローバルにキャッシュする。
- C++の例外が漏れ出さないように気を付ける。逆に、Java側の例外が発生したまま継続しない。
- finalizeが呼び出されない可能性に気を付ける。
- UnsatistiedLinkErrorで泣かない。
- extern "C"を付けたか?
- メソッド名・クラス名は正しいか?
- javahを試したか?
- System.loadLibraryを忘れていないか?
- ぶっちゃけJNIは生産性が低いので、出来ることなら使わないほうがいい。(今日のオチ)
お粗末さまでした。
JNIとC++例外
JNI関連の話が続きます。
今日は短いですが、JNIとC++例外について。
最近ではAndroid NDKもC++の例外(やRTTI)に対応していて、そのおかげで例外が必要なSTLなどのライブラリを自由に使えるようになっています。しかし、Java側はC++の例外を理解してくれないので、もしJNIを介した関数がネイティブ領域で例外をキャッチし損ねると、すぐにSegmentation Faultで落ちます。
解決策としては、JNIから呼び出されたC++側の内部処理を、noexceptな関数に閉じ込めてしまえばよいわけです。もちろんエラーが発見できないのもまずいので、JNIを使ってJava側の例外に変換してしまいます。
JNIEXPORT void JNICALL Java_Foo_bar (JNIEnv* env, jobject thisj) { jni_util::convertException(env, [=] () { // do something with env and thisj // ... throw std::runtime_error("Hoge failed!"); // C++ exception thrown! // ... }); // converts C++ exceptions to Java exceptions }
地味にC++11のラムダキャプチャの恩恵を得られる例でもあります。関数1つを見るとわずかな差ですが、クラスが増えてくると管理も大変になってくるので、記述量を減らす努力は重要です。
実装は以下のように非常に単純。例によってGitHubにも同じものがあります。
https://github.com/meryngii/jni_util
inline void throwRuntimeException(JNIEnv* env, const char* what) { jclass classj = env->FindClass("Ljava/lang/RuntimeException:"); if (classj == nullptr) return; env->ThrowNew(classj, what); env->DeleteLocalRef(classj); } template <typename Func> inline void convertException(JNIEnv* env, Func func) noexcept { try { func(); } catch (std::exception& e) { throwRuntimeException(env, e.what()); } catch (...) { throwRuntimeException(env, "Unindentified exception thrown (not derived from std::exception)"); } }
ここで、Java側の例外が発生しても、C++側はすぐに終了しないという重要な問題があります。JNIの関数を呼び出したときには、常に例外が発生しているかどうかを検査しなければいけません。こんな感じで。
class JavaException : public std::exception { }; // 例外が発生しうるJNIコールの後で呼ぶ inline void checkException(JNIEnv* env) { if (env->ExceptionOccurred()) throw JavaException(); }
上と組み合わせると、C++側で例外を利用したコードを書きつつも、Java側の事情にも配慮することができるというわけです。
今回は次回の布石で、次回はJavaから利用できるようC++のクラスをラップする方法について書きます。
参照カウントとバッファ再利用
std::shared_ptrの参照カウントを、バッファメモリの再利用に応用できるのではないかというお話。例として、単純なリソースクラスとそれのローダを考えてみます。
#include <vector> #include <memory> #include <numeric> class Resource { public: std::size_t size() const { return data_.size(); } void resize(std::size_t size) { data_.resize(size); } std::vector<int>::iterator begin() { return data_.begin(); } std::vector<int>::iterator end() { return data_.end(); } private: std::vector<int> data_; }; class Loader { public: std::shared_ptr<Resource> load(); private: std::shared_ptr<Resource> resource_; }; std::shared_ptr<Resource> Loader::load() { if (!resource_.unique()) // <=== resource_ = std::make_shared<Resource>(); resource_->resize(100); std::iota(resource_->begin(), resource_->end(), 0); // prepares data return resource_; } void f() { Loader loader; // load and release for (int i = 0; i < 100; i++) { auto resource = loader.load(); // "loader" reuses "resource_" // because "resource" is released here. } } void g() { Loader loader; // load and store std::vector< std::shared_ptr<Resource> > gotResources; for (int i = 0; i < 100; i++) { auto resource = loader.load(); gotResources.push_back(resource); // "loader" does not reuse "resource_" // because it's still shared with gotResources, } // ... }
毎回同じデータが返ってくるくだらないコードですが…。
話の肝は<===と書いてあるところの2行だけです。要はshared_ptrの指している先がunique(=参照カウントが1)だったら、自分しか使ってない訳だからリソースを再利用してもいいじゃん、というわけです。
参照カウントが0(=何も持っていない)ときにもuniqueにはならないので、nullの場合も自動的に確保されます。
大きなデータを受け取る場合は、利用側であらかじめ一時バッファ変数を用意しておいて、内部処理でそこに入れてもらうというやり方が一番よく使われますし簡単です。しかし、メモリを再利用したい場合は利用側が適切に管理する必要がありますし、バッファの型も一つに固定されます。上の例のようにshared_ptrで利用者にバッファを返却し、その利用状況を逐一監視しておけば、もう少し柔軟に運用できるんじゃないかというわけです。
shared_ptrの参照カウントを、単なるメモリの確実な解放だけでなく別の面白い用途に使えないかなあと考えたお話でした。
もっとも、上の例は一番単純な例で、もう少しうまくやるにはバッファプールなどを作る必要があると思います。ちなみに、weak_ptrだと解放されてしまうのでうまくいきません。
JNIの自動リソース解放
久々に記事を書こうと思い立ったので書きます。ブログという形態が情報共有に適切なのかどうかはさておいて、小さなひらめきであっても何らかの形で記録していかなければ自分の糧にならない、と改めて感じている今日この頃です。
最近Android NDKで作業する機会を得ていまして、C++をモリモリ書ける喜びを感じています。Androidデバイス本来の力を生かすにはネイティブコードは現状必須です。初期のAndroidのモッサリ感は、限られた資源の中でわざわざJava VM上にアプリケーションを実行しているせいだったのではないかと勘ぐることもしばしばです。
ネイティブコードを利用したAndroidアプリケーションを作るにはJNI (Java Native Interface)を使うことが必須となるのですが、このJNIがいろんな意味で非常に厄介です。JNIの問題点を適当にあげつらってみます。
- グルーコードを書くのが非常に面倒。(アプリケーションの本質でないところに労力をさく必要)
- ネイティブ領域で各種IDをいちいちキャッシュしないと実行速度が大幅に低下する。
- JavaもC++も静的コンパイルされる言語なのに、実行時まで正常に関数が呼び出されるかわからない。(失敗するとUnsatisfiedLinkErrorがthrowされる)
- ローカル参照は512個まで。(DeleteLocalRefで解放せずにそれ以上確保しようとすると即死)
以上のような内容に関して、IBMのサイトの以下の記事が参考になります。多くのこういったバッドノウハウ系の記事は読む気がしないものですが、この記事に関しては隅々まで非常に実践的な内容なのでおすすめです。
http://www.ibm.com/developerworks/jp/java/library/j-jni/
今回は、文字列と配列アクセスのグルーコードを書く手間を削減する方法についてです。大した内容ではないですが、一応ソースコードはGitHubにあげてあります。
https://github.com/meryngii/jni_util
Javaから渡される文字列と配列へのアクセスには、事前にネイティブ領域にロードする関数を呼び出す必要があります。そしてさらに、ネイティブ関数が終了するときには解放処理の関数をまた呼び出す必要があります。こんな感じで。
extern "C" JNIEXPORT void JNICALL Java_Foo_bar (JNIEnv* env, jobject thisj, jintArray arrayj) { jboolean isCopy; int* elements = env->GetIntArrayElements(arrayj, &isCopy); // Do something with array... env->ReleaseIntArrayElements(arrayj, elements, 0); }
C++プログラマとしてはRelease系の関数を直接呼び出したくないもので、これはRAII的にデストラクタで処理させるものでしょう。同じ考えのコードは探してみたらありました。
JNI C++ templates to acquire/release resources
あといちいち要素の型名を書かなければいけないのも気に食わないですね。ここはテンプレートでうまいことやるべきでしょう。というわけで以下のように。
#include "jni_util.hpp" extern "C" JNIEXPORT void JNICALL Java_Foo_bar (JNIEnv* env, jobject thisj, jintArray arrayj) { jni_util::CopiedArrayAccess<jint> array(env, arrayj); int* elements = array.elements(); size_t size = array.size(); // Do something with array... // Destructor automatically releases the resource }
まあ結局jintとか書いてる訳ですが…。本当はちゃんとiteratorをつけてコンテナとしての要件を満たすようにしようかなとも思うのですが、とりあえず実用上はこれで十分でしょう。
JNIでのJava配列へのアクセスには2種類あって、
- Get***ArrayElementsで一気にコピーする
- Get***ArrayRegionで少しずつコピーする
の2種類あります。それぞれ適当にCopied, Regionalと名前をつけて対応させておきました。
Release時の第3引数はmodeで、0, JNI_COMMIT, JNI_ABORTから選べます。0にしてReleaseしないと変更した値がJava側で反映されません。読み出しのみの場合はJNI_ABORTでOKです。*1modeに関してもコンストラクタに与える第3引数として付加してあります。
とりあえずこんなところで。
*1:JNI_COMMITって何のために使うんだろう…。