第十報:マルチヴァリューの扱い方
どうも、最近忙しくて更新できない状態が続いてますが、先日某氏が出されたプログラム本に刺激を受けてちょっとだけ復活です。
前々から氏は関数が複数の値を返せないことに不満を持っていた(リンク先第2C回)ようで、この本においてもそのことが触れられています。
そこで今回のプログラマーの友では、この複数の値を持った値「マルチヴァリュー」について考えてみたいと思います。
マルチヴァリューを扱う時に先ず考え付くのが、クラスを作ってそれを返すというものです。いろいろな型に対応できるよう、クラステンプレートにしておきます。
template <typename t_value1, typename t_value2> class CMultivalue2 { private: t_value1 m_value1; t_value2 m_value2; public: CMultivalue2(t_value1 value1, t_value2 value2) : m_value1(value1), m_value2(value2) { } CMultivalue2() { } t_value1& Value1() { return m_value1; } t_value2& Value2() { return m_value2; } }; |
これで一応扱えますが、これだけだと扱いにくいことが分ります。それは、返された値を Value1 や Value2 とかいう意味のない名前で扱わなくてはならないからです。そこで別の変数に代入することになるのですが、そうなると戻り値を受けるために使った CMultivalue2 オブジェクトとそれらの変数の両方が存在するという事になります。
そこで、いちいち CMultivalue2 オブジェクトを作らなくても値を個々の変数に返せるようにします。これは istream と同じように >> 演算子をオーバーロードすることで実現できます(別の演算子でも構いませんが、よく使われているので >> にします)。
これを実現するにはちょっとした工夫が必要です。例えば
foo() >> a >> b;
とした時、初めの >> 演算子の戻り値は何になるのでしょうか? 当然 >> 演算子を使って b に代入ができるようなものでなくてはならないので、また別のクラスのオブジェクトを返す必要があることが分ります。つまり、このクラスも作っておく必要があるわけです。
具体的にはこんな感じです。
// 2 つ目の値を取得するために使うクラステンプレート template <typename t_value1> class CMultivalue1 { private: t_value1 m_value1; public: explicit CMultivalue1(t_value1 value1) : m_value1(value1) { } CMultivalue1() { } // 2 つ目の値を返す void operator>>(t_value& value1) { value1 = m_value1; } }; // 1 つ目の値を取得するために使うクラステンプレート template <typename t_value1, typename t_value2> class CMultivalue2 { private: t_value1 m_value1; t_value2 m_value2; public: CMultivalue2(t_value1 value1, t_value2 value2) : m_value1(value1), m_value2(value2) { } CMultivalue2() { } // 1 つ目の値を返す CMultivalue1<t_value2> operator>>(t_value& value1) { value1 = m_value1; return CMultivalue1(value2); } }; |
これで一応 >> で扱えるようにするという目的は達することができました。しかも、こうしておけば 1 つ目の値だけが欲しい場合、2 回目の >> をしなければいいだけになります。
foo() >> a; // 2 つ目の値は無視!
しかし、これだけで終わっては「マルチ」ヴァリューとは呼べません。3 つも 4 つも値を返したい場合に使えないからです。
ここで「あー、3 つ返す用のクラスとか 4 つ返す用のクラスとか作るのかー、面倒いなー」と思うわけですが、実のところそんなものは必要ありません。なぜなら、クラステンプレートをネストして使えばいいだけだからです。しかし、上のままではネストして使えるものにはなってないので、ちょっと改造してやります。
// 最後の値を取得するために使うクラステンプレート template <typename t_value1> class CMultivalue1 { private: t_value1 m_value1; public: explicit CMultivalue1(t_value1 value1) : m_value1(value1) { } CMultivalue1() { } // 最後の値を返す void operator>>(t_value& value1) { value1 = m_value1; } }; // 最後以外の値を取得するために使うクラステンプレート template <typename t_value1, typename t_value2> class CMultivalue2 { private: t_value1 m_value1; t_value2 m_value2; public: CMultivalue2(t_value1 value1, t_value2 value2) : m_value1(value1), m_value2(value2) { } CMultivalue2() { } // 値を返して次に進む t_value2& operator>>(t_value& value1) { value1 = m_value1; return value2; } }; |
これで、 CMultivalue2 の t_value2 に CMultivalue1 か CMultivalue2 を指定することで、何個でも対応できるようになります。
しかし、実際直接ネストして使うのは面倒なので、次のようなマクロと関数テンプレートを用意しておくと良いでしょう。
// 1つの値用のマルチ(?)ヴァリュー型 #define Type1(t_value1) \ CMultivalue1 <t_value1> // 2つの値用のマルチヴァリュー型 // ) と > の間の空白を消してはいけない! #define Type2(t_value1, t_value2) \ CMultivalue2 <t_value1, Type1(t_value2) > // 3つの値用のマルチヴァリュー型 // ) と > の間の空白を消してはいけない! #define Type3(t_value1, t_value2, t_value3) \ CMultivalue2 <t_value1, Type2(t_value2, t_value3) > // 1つの値用のマルチ(?)ヴァリューオブジェクトを返します template <typename t_value1> inline Type1(t_value1) Value1(t_value1 value1) { return Type1(t_value1)(value1); } // 2つの値用のマルチヴァリューオブジェクトを返します template <typename t_value1, typename t_value2> inline Type2(t_value1, t_value2) Value2(t_value1 value1, t_value2 value2) { Type1(t_value2) multi(Value1(value2)); return Type2(t_value1, t_value2)(value1, multi); } // 3つの値用のマルチヴァリューオブジェクトを返します template <typename t_value1, typename t_value2, typename t_value3> inline Type3(t_value1, t_value2, t_value3) Value3(t_value1 value1, t_value2 value2, t_value3 value3) { Type2(t_value2, t_value3) multi(Value2(value2, value3)); return Type3(t_value1, t_value2, t_value3)(value1, multi); } |
Value 関数内でいちいち変数を介して渡しているのは、コピーコンストラクタ以外ではテンポラリオブジェクトの参照渡しを許可していないコンパイラがあるからです(例:g++)。
こうして無事マルチヴァリューが扱えるようになりました。6 つまでのマクロ・関数テンプレートを用意したヘッダファイルをここ (multivalue.h) に置いておきます。自由に使って下さって結構です。
multivalue.h ではもうちょっと改造してあって、次のような仕様を追加してあります。
以上のマルチヴァリューの機能を、実際の使用例を見ながら解説していきましょう。
マルチヴァリュー型は CMultivalue1/2 を直接使わずに、Type 系マクロを使って扱います。例えば int と char を扱うマルチヴァリュー型は Type2(int, char) 、const char* と int と LRESULT を扱うマルチヴァリュー型は Type3(const char*, int, LRESULT) となります。マクロは TypeN(...) という形になっており、型の個数を N に指定して使います。マクロを使ってるのでインテリセンスが効かなくなるのがちょっと難点ですが...。
そして、実際のマルチヴァリューオブジェクトを生成するには Value 系関数テンプレートを使います。関数テンプレートはテンプレート引数を暗黙に指定してくれますが、リテラル文字列は const char* ではなく char* になる点など、暗黙のままでは困る場合もあります。その時は明示的に指定してください。
例えば、次のようになります。
Type2(const char*, bool) Season(int nMonth) { switch(nMonth) { case 3: case 4: case 5: return Value2<const char*>("春", true); case 6: case 7: case 8: return Value2<const char*>("夏", true); case 9: case 10: case 11: return Value2<const char*>("秋", true); case 12: case 1: case 2: return Value2<const char*>("冬", true); default: return Value2<const char*>(NULL, false); } } |
1 つ目の値は季節を表す文字列へのアドレスで、2 つ目の値は関数が成功したかどうかです。
この関数を Get メンバと組み合わせて使えば、次のようなことができます。
if((Season(nMonth) >> psMonth).Get()) cout << psMonth << endl;
関数が成功したかどうかを変数に保存しなくても利用できるわけです。
また、場合によっては途中の値だけしか取得する必要のない場合もあるかもしれません。そういう場合には Skip メンバが使えます。
foo().Skip() >> a >> b; // 2 つ目の値から取得 (foo().Skip() >> a).Skip() >> b; // 2 つ目と 4 つ目の値を取得
途中でスキップする時はカッコが必要になるのがちょっと注意するところです。
あと、別にマルチヴァリューオブジェクトをマルチヴァリューオブジェクトのまま使っても構いません。
Type3(int, int, int) hoge3(Value3(1, 2, 3)); // 初期化 Type2(int, int) hoge2(hoge3.Skip()); // 1 つ飛ばしたマルチヴァリューを複製 cout << hoge2.Get() << hoge2.Skip().Get() << endl; hoge2 = GetHoge(); // GetHoge の戻り値の型は Type2(int, int) hoge2.Skip() >> a;
意外と自由度が高いことに自分でもちょっとびっくりです。
こんな感じで使えるマルチヴァリューですが、速度のほうはどうなのでしょうか?
全ての関数は inline にしてあるので、ちゃんと inline 展開されれば速いです。そりゃぁもう速いです。例えば次のような場合...
inline Type2(int, int) OneTwo() { return Value2(1, 2); } int main() { int a, b; OneTwo() >> a >> b; printf("%d, %d\n", a, b); return 0; } |
直接 printf に 1 と 2 を渡すようなコードが出力されます。
int main() { int a, b; OneTwo() >> a >> b; printf("%d, %d\n", a, b); 00401000 push 2 00401002 push 1 00401004 push offset string "%d, %d\n" (4060ECh) 00401009 call printf (401020h) 0040100E add esp,0Ch return 0; 00401011 xor eax,eax } 00401013 ret |
というわけで、基本的には速度を気にする必要はないでしょう。
ただ、クラスを渡す時は気をつける必要があります。なぜなら、コピーコンストラクタがアホ程呼ばれるからです。
例えば次のようなコードを実行してみると...
class A { public: A(){ } public: A(const A&){ cout << "A" << endl; } }; class B { public: B(){ } public: B(const B&){ cout << "B" << endl; } }; class C { public: C(){ } public: C(const C&){ cout << "C" << endl; } }; inline Type3(A, B, C) Abc() { A a; B b; C c; return Value3(a, b, c); } int main() { Abc(); return 0; } |
なんと、値の取得も行っていないのに A は 2 回、B は 4 回、C に至っては 6 回もコピーコンストラクタが呼ばれます。コピーコンストラクタに new なんかがあるクラス(例:string)を渡す場合、これは致命的です。
それを無視してもいいというのであれば別に使ってもらっても構いませんが、コピーコンストラクタが複雑なクラスを返す場合にはあまり使わないほうがいいかもしれません。逆に、コピーコンストラクタが inline で中身も標準型の代入のみであれば、普通の変数と同じくらい高速に処理してくれることでしょう。
あと、配列は返せません。一応構造体を介して間接的に返すことはできますが、コピーが発生するのでやめた方がいいです。
というわけで今回は C++ でマルチヴァリューを扱う方法について考えたわけですが、どんなもんでしょうか。何か「それを使うのはやばい!」という点でもあれば報告お願いします。
それでは、また。
よーく考えてみたのですが、いちいち新しいクラステンプレートを作ったのでもさほど手間はかからないような気がしてきました(笑)。確かに上記の方法では新しいクラステンプレートを作る必要はありませんが、実際問題 Type 系マクロを作ることになるので、クラステンプレートを増やしたのでも手間にさほど差はないのではないのかと思います。コンストラクタの引数を参照渡しにしたいのでやはり Value 系関数は用意する必要がありますが、コンストラクタにダダダっと参照渡しするだけなので、コピーコンストラクタもさほど呼ばれません。さらにインテリセンスも効くし、新しいクラステンプレートを作った方が便利かも、と思ったのです。
ただ、>> 演算子を使って変数に直接返すことができる方法が開発できたという意味においては、今回の話は無駄ではなかったと思います。
というわけで、クラステンプレートを作るタイプのも用意してみました(multivalue2.h)。実際問題 6 つまで使えれば困ることもないと思うので、こんなもんでいいんでないでしょうか? 実のところまともにやっても大した手間じゃなかったというオチがついたところで、追記を終わらせていただきます。
Last update was done on 2002.6.9