C++のクラスは魔法の箱?(1)
オブジェクト指向プログラミングの醍醐味の1つは、自分が提供するクラスに高い機能をスマートで格好いいインタフェース仕様で組み込めることだと思います。
優れたものは適切にギミックを組込み、あたかもマジックボックスのようでもあります。プログラマのセンスや腕の見せ所、重要なアピールポイントになります。
更に、map クラスに至っては、添え字の中に文字列等を指定できる連想配列を実現しています。
もし、クラスが提供するファンクションが全て普通の関数タイプだけだとしたら味気ありません。演算子を再定義できればコードの視認性を格段に高められますし、使う側に楽しさを提供することさえ可能なのです。
これは、一見オブジェクト指向そのものとは無関係に思われるかも知れません。
しかし、実は間接的に関係があるのです。
組込みの型、たとえば int であるとか char であるとか、これらもC++では型であると同時にクラスでもある、と考えるのです。型=クラスと言ってもいいのです。
つまり、クラスを作ることはユーザ定義型を作ることでもあるのです。であるならば、組込み型と同様にユーザ定義型にも演算子が使えないと、それを使用する箇所での見た目が全く違うものになってしまい、ユーザ定義型と言うには少々無理がある、ということになってしまうのです。(ただし、一般的にはクラスを完全な組込み『型』のように綺麗に設計できるケースは少ないと言えます。)
演算子を使う場合と関数をコールする場合の違いを標準ライブラリの string クラスの場合で見てみましょう。
なお、この append() は実際に存在している関数です。なぜ、+= 演算子が使えるのに、と思われるかも知れませんが、使用する側の都合で演算子よりも関数を使う方が適切と判断される場合があるからかも知れません。
たとえば、append() には引数を複数指定する機能の詳細が異なるバージョンが多重に定義されています。そういった他の append() と並列に使用する場合、1カ所だけ += 演算子を使うことはせず、append() で揃えたい、といった場合が考えられます。
この記事ではC++の文法自体はあまり説明してきませんでしたが、演算子を再定義する方法はここでご紹介しておきます。演算子を関数を使って再定義するには operator というキーワードを使います。
+= 演算子を再定義する例を示します。
それと、再定義と言っていますが、コード2の場合、Point クラス同士の演算の場合に限られます。それ以外の組み合わせには影響しません。
また、演算子は + なのに、中身では引き算をする等、使用者を混乱させるような定義は避けなければなりません。これは、故意に行わなくても、特に複雑な処理になる場合など、結果的に演算子の意味をこじつけてしまい、直感的に理解しにくい動作を定義してしまうことはありがちです。そのような場合は無理をせず、通常の関数として実装した方が好ましいかも知れません。この辺りのさじ加減はプログラマのセンスが問われるところでもあります。
優れたものは適切にギミックを組込み、あたかもマジックボックスのようでもあります。プログラマのセンスや腕の見せ所、重要なアピールポイントになります。
演算子関数
C++標準ライブラリでさえそうなのです。たとえばこれまでの記事で扱ってきた vector クラスは、まるで言語組込みの機能のように、添え字([])を使って配列の要素にアクセスできる、というインタフェースを提供しています。更に、map クラスに至っては、添え字の中に文字列等を指定できる連想配列を実現しています。
もし、クラスが提供するファンクションが全て普通の関数タイプだけだとしたら味気ありません。演算子を再定義できればコードの視認性を格段に高められますし、使う側に楽しさを提供することさえ可能なのです。
これは、一見オブジェクト指向そのものとは無関係に思われるかも知れません。
しかし、実は間接的に関係があるのです。
組込みの型、たとえば int であるとか char であるとか、これらもC++では型であると同時にクラスでもある、と考えるのです。型=クラスと言ってもいいのです。
つまり、クラスを作ることはユーザ定義型を作ることでもあるのです。であるならば、組込み型と同様にユーザ定義型にも演算子が使えないと、それを使用する箇所での見た目が全く違うものになってしまい、ユーザ定義型と言うには少々無理がある、ということになってしまうのです。(ただし、一般的にはクラスを完全な組込み『型』のように綺麗に設計できるケースは少ないと言えます。)
演算子を使う場合と関数をコールする場合の違いを標準ライブラリの string クラスの場合で見てみましょう。
コード1
void func1() { string a = "abcd"; string b = "1234"; a += b; // "abcd1234" } void func2() { string a = "abcd"; string b = "1234"; a.append(b); // "abcd1234" }func1()では演算子を使っているので、文字列の接続を直感的に理解できるコードになっています。func2()は、+=と同じ動作をする append() メンバ関数を使っていますが、append という単語の意味を知っていたとしても、やはり演算子の視認性に比べれば劣ります。ぱっと見が大切なのです。
なお、この append() は実際に存在している関数です。なぜ、+= 演算子が使えるのに、と思われるかも知れませんが、使用する側の都合で演算子よりも関数を使う方が適切と判断される場合があるからかも知れません。
たとえば、append() には引数を複数指定する機能の詳細が異なるバージョンが多重に定義されています。そういった他の append() と並列に使用する場合、1カ所だけ += 演算子を使うことはせず、append() で揃えたい、といった場合が考えられます。
この記事ではC++の文法自体はあまり説明してきませんでしたが、演算子を再定義する方法はここでご紹介しておきます。演算子を関数を使って再定義するには operator というキーワードを使います。
+= 演算子を再定義する例を示します。
コード2
// 座標クラス class Point { int m_x; int m_y; public: // + 演算子の定義 Point & operator += (const Point & rPoint) { m_x += rPoint.m_x; m_y += rPoint.m_y; return * this; // 自分自身を返す } } int main() { Point a, b; a += b; return 0; }このように、operator というキーワードの次に再定義する演算子を記述し、合わせて関数名のようになります(ただし、呼出す方は関数呼出の書式は使えません)。再定義できる演算子は限られていて、全てを書き換えられるわけではありません。演算子の優先順位を変えることもできません。
それと、再定義と言っていますが、コード2の場合、Point クラス同士の演算の場合に限られます。それ以外の組み合わせには影響しません。
また、演算子は + なのに、中身では引き算をする等、使用者を混乱させるような定義は避けなければなりません。これは、故意に行わなくても、特に複雑な処理になる場合など、結果的に演算子の意味をこじつけてしまい、直感的に理解しにくい動作を定義してしまうことはありがちです。そのような場合は無理をせず、通常の関数として実装した方が好ましいかも知れません。この辺りのさじ加減はプログラマのセンスが問われるところでもあります。
0 件のコメント:
コメントを投稿