ホーム   SikiWiki 

C++プログラミング手法

categories/C++プログラミング手法




型ごとの番号付け

意図

動的に、型ごとに番号付けをする。

動機

C++では、動的に型の独自性をチェックする場合はtype_infoクラスを使用する。 type_infoはtypeid 演算子を使用することで取得することができる。type_infoは順序付けされており、比較演算子などで比較することで、同じクラスかどうかを比較することができる。

しかし、type_infoは整数としては扱えないため、配列のインデックスアクセスなどでは使用できない。また、プログラム中で同じ比較しかできないため、指定した順番にしたいなどの要求に応えることはできない。

解法とサンプルコード

小粋なカウンタ(Nifty_Counter)、及びテンプレート関数、静的局所変数を組み合わせて、型からunsigned intへの変換を実現する。

// Numbering.hpp template<typename derived_t> class Numbering { public: template<typename type_t> static unsigned int number() { static unsigned int r(count()); return r; }; private: static unsigned int count() { static unsigned int r(0); return r++; }; }; // main.cpp class A : public Numbering<A> {}; class B : public Numbering<B> {}; int main() { A::number<int>(); //0 ……(1) A::number<int*>(); //1 A::number<int&>(); //2 A::number<const int>(); //3 A::number<const int*>(); //4 A::number<const int&>(); //5 A::number<int>(); //0 ……(2) A::number<int*>(); //1 A::number<int&>(); //2 A::number<const int>(); //3 A::number<const int*>(); //4 A::number<const int&>(); //5 B::number<const int&>(); //0 B::number<const int*>(); //1 B::number<const int>(); //2 B::number<int>(); //3 B::number<int*>(); //4 B::number<int&>(); //5 B::number<const int&>(); //0 B::number<const int*>(); //1 B::number<const int>(); //2 B::number<int>(); //3 B::number<int*>(); //4 B::number<int&>(); //5 };

Numberingテンプレートが指定された型ごとにナンバリングを行うテンプレートである。 ここでは汎用性を高めるため奇妙に再帰したテンプレートとしている。

Numberingは静的テンプレートメンバ関数numberを持つ。numberは型ごとに異なった関数を実体化する。また、numberは関数内に静的局所変数rを持つ。

静的局所変数は、コンパイル時に判別可能な定数を指定されていない場合は、その関数が実行される時に初期化される。すなわち、上記例の(1)のように、各々の型ごとのメンバ関数が初めて実行される時に初期化される。

静的局所変数rは、静的メンバ関数countからの戻り値で初期化される。静的メンバ関数countは呼び出されるごとにインクリメントされた別の整数を返すので、rにはそれぞれの型ごとに別々の整数が割り当てられる。

また、以前に指定された型でnumberが呼び出された場合(2)は、rは既に初期化済のためcountは実行されず、すでに割り当てられたrの値をそのまま返す。

この結果、Numberは型ごとに異なった整数を返すことができる。





制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)

クラスと関数の紐付け

意図

関数がどのクラスに関係付けられているかを識別する。

動機

C++では継承により基底クラスのメンバー関数を引き継ぐことができるが、時には各クラスに特定のメンバー関数の定義を強制したいことがある。特に必要になるのが『ファクトリー関数』を使用する場合である。

『ファクトリー関数』とは、あるクラスのインスタンスを生成する静的メンバー関数のことである。

通常、C++ではインスタンス生成時の挙動や設定をコンストラクタで行うが、このオブジェクト初期化は様々な問題を引き起こすことがある。特にコンストラクタ内ではメンバー仮想関数が仮想的には振る舞わないため、初期化段階で高度な作業を行おうとしてもできないことがある。

そのような場合は、1)オブジェクトを生成し、2) 生成したオブジェクトを加工し、3) そのオブジェクトを戻す『ファクトリー関数』と呼ばれる静的メンバー関数を使用することがある。

ファクトリー関数は生成後のオブジェクトを加工するため、コンストラクタに係わる問題を回避することができる。しかし、ファクトリー関数はpublicに指定する必要があるため、派生クラスでファクトリー関数を定義し忘れた場合、派生クラスのインスタンスを作成しようとして基底クラスのインスタンスを生成してしまうというバクを作り出してしまう。

派生クラスのファクトリー関数を指定したときに基底クラスのファクトリー関数にアクセスしなければ問題は発生しないが、publicの可視性よりアクセスを防ぐことはできない。

解法とサンプルコード

次善策として、「ある関数がどのクラスと関連付けられているかを識別し、ある関数に既に関連付けられているクラスが異なる場合にエラーとする」ことでメンバー関数の定義忘れを防止する。

ただし、基底クラスを登録し忘れると、派生クラスでメンバー関数を定義してなくてもそのまま「派生クラス:基底クラスの関数」という組で登録されてエラーにならないため、注意が必要。

// Registry.hpp class TRegistry { public: template<class type_t> static bool registed() { return registed<type_t>(false); }; template<class type_t> static unsigned int regist() { if (registed<type_t>()) return number<type_t>(); assert((check<type_t, boost::shared_ptr<type_t> (*)(), type_t::instance>() && "TArchive Error. No 'instance' method.")); assert((check<type_t, std::string const&(*)(), type_t::name>() && "TArchive Error. No 'name' method.")); registed<type_t>(true); return number<type_t>(); }; private: TRegistry(); // TRegistryのインスタンスは生成しない。Monostateパターン static unsigned int count() { static unsigned int r(0); return r++; }; template<class type_t> static unsigned int number() { static unsigned int r(count()); return r; }; template<typename member_t, member_t member> static unsigned int number(unsigned int n) { static unsigned int r(n); return r; }; template<class type_t, typename member_t, member_t member> static bool check() { return (number<type_t>() == number<member_t, member>(number<type_t>())); }; template<class type_t> static bool registed(bool migrate) { static bool registed(false); if ((!migrate)||(registed)) return registed; bool t(registed); registed = true; return t; }; }; template<class target_t> struct TRegister { TRegister() { static regist r; }; private: struct regist { regist() { TRegistry::regist<target_t>(); }; }; }; // main.cpp struct A { static boost::shared_ptr<A> instance() { return boost::shared_ptr<A>(new A); }; static std::string const& name() { static std::string n("A"); return n; }; }; struct B : public A { static boost::shared_ptr<B> instance() { return boost::shared_ptr<B>(new B); }; static std::string const& name() { static std::string n("B"); return n; }; }; struct C : public A { static boost::shared_ptr<C> instance() { return boost::shared_ptr<C>(new C); }; }; namespace { TRegister<A> a; TRegister<B> b; TRegister<C> c; // assert error } int main() { };

TRegistryクラスがクラスチェック用のクラスで、TRegisterクラスはTRegistryクラスにクラスを登録するためのヘルパークラスである。

TRegisterクラスはインスタンス生成時に指定クラスをTRegistryクラスに登録する。そのため、グローバルスコープに実体を用意することで、プログラム実行直後にクラスを確認することができる。

templateTRegistry::regist() でクラスのチェックを行っている。

ここでは、boost::shared_ptr instance()とstd::string const& name()のチェックを行っている。

template static bool check()が関数とクラスの紐付けを確認している。

template static unsigned int number()はクラスのナンバリングを行っている。このテンプレート関数は、テンプレート引数に指定されたクラス毎に1つずつ、それぞれユニークな関数を実体化する。テンプレート関数にあるローカルな静的変数rも、実体化された関数それぞれに1つだけ存在する。rは実体化した関数が最初に実行されるタイミングで初期化される。初期化の時には、実行するごとに1ずつインクリメントした数字を返すcount()で初期化しているため、結果として実体化した関数毎にユニークな数字が付けられる==登録したクラス毎にユニークな番号が付けられる。

template static unsigned int number(unsigned int n) は関数と番号の紐付けを行っている。戻り値はローカルな静的変数のため、一度初期化されるとunsigned int nで指定された数字にかかわらず同じ数字を戻す。

check()ではnumber()とnumber(number())の番号の比較を行っている。

number(number())は最初に登録したnumber()を戻すため、結果として A)指定クラスと B)指定関数に関連付けられた指定クラスが、最初に登録されたときと同じかどうかを判定することができる。





制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)

動的型変数とマルチメソッド

意図

動的型変数と、その変数を引数とするマルチメソッドを提供する。

動機

C++では、静的型のチェックによりコンパイル時にプログラムの整合性を判定できるようになっている。しかし、全ての型がコンパイル時にチェックされるため、徐々に機能を追加していく開発スタイル(スクリプト言語などで良く行われている)だと、この厳しい型チェックのせいで柔軟なプログラミングが困難となっている。

この問題を解決するため、C++ライブラリの一つであるboostでは、動的型変数boost::anyを用意している。しかし、boost::anyはその変数を処理するときに保存してあるデータの型を正確に指定する必要があり、柔軟性が高いとは言えない。

また、Modern C++ Designでは動的に実行するメンバ関数を変更するマルチメソッドの技法について言及しているが、最高速の定数時間のマルチメソッドを実現する手法はクラスそのものに手を加える必要があり、汎用性に劣っていた。

そこで、この二つの技法を組み合せることにより、汎用性が高く、また速度的にも優れた動的変数/マルチメソッドを実装する。

解法とサンプルコード

boost::anyに、型ごとの番号付けの手法を使用してナンバリングを行う機能を付加し、Modern C++ Designで言及されている最高速の定数時間のマルチメソッドを実装する。なお、下記の実装は単純化しているため、詳細はこのページを参照のこと。

なお、Modern C++ Designの対数時間のマルチメソッドならば、boost::anyをそのまま流用してマルチメソッドを実装することが可能である。

// Holder.hpp struct MultiMethod { MultiMethod() {}; public: template<typename R, typename M, typename H, typename A1> static bool entry() { struct Local { static R trampoline(M& self, H& arg1) { // (b) return self(holder_cast<A1>(arg1)); }; }; typedef R (*T)(M&, H&); static bool t((find<T>(Holder::id<A1>()) = &Local::trampoline) != 0); return true; }; template<typename R, typename M, typename H> static R apply(M& method, H& arg1) { typedef R (*T)(M&, H&); T t(find<T>(arg1.id())); if (!t) throw BadMultiMethod(); return t(method, arg1); }; private: //////////////////////////////////////////////////////////////////////// template<typename T> static T& find(const unsigned int pos1) { static std::vector<T> r; // (a) if (r.size() <= pos1) { r.resize(pos1+1, 0); } return r[pos1]; }; }; /////////////////////////////////////////////////////////////////////////// // Original any for fast multi method /////////////////////////////////////////////////////////////////////////// class Holder { public: Holder() : caddy_(0) {}; template<typename type_t> Holder(const type_t& value) : caddy_(new Container<type_t>(value)) {}; Holder(const Holder& other) : caddy_(other.caddy_ ? other.caddy_->clone() : 0) {}; ~Holder() { delete caddy_; }; Holder& swap(Holder& other) { std::swap(caddy_, other.caddy_); return *this; }; template<typename type_t> Holder& operator=(const type_t& other) { Holder(other).swap(*this); return *this; }; Holder& operator=(const Holder& other) { Holder(other).swap(*this); return *this; }; bool empty() const { return !caddy_; }; const std::type_info& type() const { return caddy_ ? caddy_->type() : typeid(void); }; template<typename type_t> type_t* cast() { return caddy_ && type() == typeid(type_t) ? &static_cast<Container<type_t> *>(caddy_)->content_ : 0; }; // additional method unsigned int id() { return caddy_ ? caddy_->id() : 0; }; template<typename type_t> static unsigned int id() { static unsigned int r(count()); return r; }; private: static unsigned int count() { static unsigned int r(0); return ++r; }; // end class Caddy { public: virtual ~Caddy() {}; virtual const std::type_info& type() const = 0; virtual Caddy* clone() const = 0; virtual unsigned int id() = 0; // additional method }; template<typename type_t> class Container : public Caddy { public: Container(const type_t& value) : content_(value) {}; virtual const std::type_info& type() const { return typeid(type_t); }; virtual Container* clone() const { return new Container(content_); }; virtual unsigned int id() { return Holder::id<type_t>(); }; // additional method type_t content_; }; Caddy* caddy_; }; class bad_holder_cast : public std::bad_cast { public: virtual const char * what() const throw() { return "bad_holder_cast: failed conversion using holder_cast"; }; }; template<typename type_t> type_t* holder_cast(Holder* operand) { return operand->cast<type_t>(); }; template<typename type_t> inline const type_t* holder_cast(const Holder * operand) { return holder_cast<type_t>(const_cast<Holder *>(operand)); }; template<typename type_t> type_t holder_cast(Holder& operand) { typedef typename remove_reference<type_t>::type nonref; nonref* result = holder_cast<nonref>(&operand); if(!result) boost::throw_exception(bad_holder_cast()); return *result; }; template<typename type_t> inline type_t holder_cast(const Holder& operand) { return holder_cast<const remove_reference<type_t>::type&>(const_cast<Holder&>(operand)); }; // main.cpp struct TestMethod { std::string operator()(Holder& value) { return MultiMethod::apply<string>(value); }; //(1) std::string operator()(int value) const { return string("int1"); }; //(2) std::string operator()(double value) const { return string("double1"); }; //(3) }; bool m00(MultiMethod::entry<std::string, TestMethod, Holder, int>()); bool m01(MultiMethod::entry<std::string, TestMethod, Holder, double>()); //(4) int main() { TestMethod0 m; Holder h0(0); m(h0); //(5)"int0" Holder h1(0.0); m(h1); //(6)"double0" h1 = 0; m(h1); //(7)"int0" h1 = 'a'; m(h1); //(8)"int0" };

Holderがboost::anyを改造した動的変数クラスで、MultiMethodクラスの静的メンバ関数がマルチメソッド用の関数群である(クラス化してグループ化・アクセス制御を行っている)。使い方は上記の通り。

Holderはboost::anyに型ごとの番号付けを行うメンバ関数idを追加したものである。定数時間のマルチメソッドでは、配列のインデックスアクセスを使用してメソッドへの定数時間アクセスを実現しているが、このマルチメソッドでも同様の技法を使用して定数時間アクセスを実現している。(なお、boost::anyの詳細は説明しない)

MultiMethod::findは、Holderに保存されたインスタンスを実際の型にキャストして実行するトランポリン関数(b)を要素とする配列(a)を、静的ローカル変数として保持する。この配列(a)は、(Holder::idにより型ごとに割り当てられた)整数をインデックスとしている。マルチメソッドは、このトランポリン関数を実行時に呼び出し、引数のHolderを実際の型に変換した上で関数を呼び出すことで実現している。

(なお、上記の配列(a)はコンパイル時に自動的に初期化することができないため、(4)のように登録用のメソッドを使用して初期化を行う)

TestMethodが今回対象となるマルチメソッドで、(2)及び(3)が動的に呼び出されるメソッドである。なお、本例では(1)のように転送関数を用意し、通常の関数オブジェクトと同様にマルチメソッドを呼び出すことができるようにしている。

実際に使用するときは、(5)-(8)のように関数オブジェクトとして使用する。メソッドの呼び出しは

  1. (1)の転送関数でMultiMethod::applyに渡す。
  2. MultiMethod::applyは、
    1. 引数のHolderからHolder::id(実体はContainer::id)を使用して、型ごとに割り当てられた整数を入手
    2. 上記整数をMultiMethod::findに渡して該当するトランポリン関数を入手する。
    3. トランポリン関数内で、Holderを実際の型に変換した上で再度関数オブジェクトを実行する。

の手順で、実行時に引数によるディスパッチを行う。





制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)