ここでは、入出力ストリームと共に使用するマニピュレータの作り方について解説します。マニピュレータは、関数でも、クラスでも実現できる少し毛色の変わったおもしろい存在です。マニピュレータを理解するにはオペレータのオーバーロードの知識が必須ですので、そちらもご覧ください。
マニピュレータとは、cin、cout などを代表とする入出力ストリームに対する、挿入演算子 << や抽出演算子 >> と協調して、入出力フォーマットの変更を行うなどの目的で設計されたもので、標準ライブラリでは endl、setw、setfill、... などが定義されています。これらの使い方を少し見てみましょう。
cout << "Hello maniac" << endl; // 改行を挿入し、出力をフラッシュする。 cout << "[" << setw(6) // 幅を 6 に制限する。 << setfill('+') // 埋め文字を + に設定する。 << 123 << "]" << endl; cout << "[" << 123 << "]" << endl; cout << "[" << setw(5) // setw(5) : 幅を 5 に制限する。 << 123 << "]" << endl;
標準ライブラリのマニピュレータの使用サンプル Manipulator1.cpp
printf のように、あらかじめ変数型に合わせてフォーマットを決めておかなくても、何でも? プリントできてしまうのは、まぁ、便利ですね。実行結果は次のようになります。埋め文字が + に設定されていることや、表示の幅が制御されていることが分かります。
Hello maniac [+++123] // 幅が 6、埋め文字が + に設定されている。 [123] // 幅はリセットされている。 [++123] // 幅は 5 になった。埋め文字はリセットされていない。
setw の効果は、次の一回の出力でリセットされることや、setfill の効果は次の setfill の呼び出しまでリセットされないことなどを理解すれば、出力のフォーマットを制御することが出来ます。
しかし、私は、個人的には、これら標準ライブラリで定義されているマニピュレータは、あまり便利だとは思えないので、ほとんど使いません。ストリームを使用しないで printf を使えば、埋め文字は設定できませんが、上記の例に近い出力は、次のように簡潔に書けてしまうし、どのように出力されるかもこの方が分かりやすいと思います。ただし、ストリームのようにスマートに出力先を切り替えるということは出来ません。
printf("Hello maniac\n"); printf("[%6d]\n", 123); printf("[%d]\n" , 123); printf("[%5d]\n", 123);
これについては、printf の利便性とスマートな出力先の切り替えの両方を満たすものとして、後で、C++マニアックオリジナルの printf と同様に使えるマニピュレータ mprintf を紹介することにします。
マニピュレータの実現方法は、数種類ありますが、ここでは、引数のないマニピュレータをグローバル関数として作成する方法について解説します。引数のある場合については引数のあるマニピュレータの作り方で解説しますので、そちらをご覧ください。
マニピュレータの作り方は、ストリームが入力であるか出力であるかによっては殆ど変わらないので、ここでは、出力ストリームのマニピュレータについて解説します。
それでは、取り掛かりましょう。C++ 標準ライブラリの ostream クラスには、このタイプのマニピュレータを定義出来るように挿入演算子 << がほぼ次のように定義されています。 注 : ですから、マニピュレータの作者がこれを定義する必要はありません。
class ostream { public: ostream& operator <<(ostream& (*pmanipulatorfunction)(ostream&)) { return (*pmanipulatorfunction)(*this); } };
上記の定義で pmanipulatorfunction がマニピュレータで、これは、関数へのポインタであり、引数として ostream& 型オブジェクトをとり、同じ型への参照を返すものです。マニピュレータの具体的な定義の方法は次のようになります。この例の ManipulatorFucntion の部分は、実際のマニピュレータの名前に置き換えるべき仮の名前です。
ostream& ManipulatorFucntion(ostream& ros) { ros を使って何かをする。 return ros; }
マニピュレータの作者が行うべきことは、マニピュレータの名前を決めて、上記の ros を使って何かをすると書かれた部分を定義することだけです。では例として、Hello maniac という文字列を ostream に挿入するマニピュレータ HelloManiac を作成してみます。
ostream& HelloManiac(ostream& ros) { ros << "Hello maniac"; return ros; }
引数のないマニピュレータの定義サンプル Manip1.cpp
マニピュレータの定義自体は、とてもに簡単ですね。念のためこれの動作を、追ってみましょう。まず、使い方は、次のようになります。
cout << HelloManiac;
上記のコードが実行されると、まず、ostream 型のオブジェクトである cout のメンバ関数である挿入演算子 << が呼び出されます。挿入演算子 << には、引数としてマニピュレータである関数 HelloManiac のポインタが渡されます。関数 << は定義に従って、次のようにマニピュレータ関数を呼び出します。
HelloManiac(cout); // *this は cout そのもの
すると、関数 HelloManiac は、ros として cout への参照をもらうので、定義に従って、次のことを実行します。
cout << "Hello maniac"; // ros は cout そのもの return cout;
上記が終了すると関数 << は戻り値 cout をそのまま返すので、元の式全体が cout になり、めでたく実行が終了します。結果として、cout に "Hello maniac" という文字列が挿入されたことになります。
この項で解説した引数のないマニピュレータをストリームと共に作成する具体例については CistreamCsv の定義 をご覧ください。
上記のマニピュレータの定義は、とても簡単で良いのですが、マニピュレータの実体がグローバル関数で出来ているため、このままでは、実質的にヘッダの中では定義できないという欠点があります。もし、ヘッダの中で定義すると、複数の cpp ファイルでそのヘッダを include した場合に、マニピュレータの複数の実体が作成されてしまい、リンク時に同じ名前の関数が多重定義されているというエラーになってしまいます。私が確かめたところでは少なくとも VC++6 ではそのようになりました。BorlandC++5.5 ではエラーになりませんでしたが、本来エラーになるべきところです。実際にエラーになる処理系があるため、実体がグローバル関数であるということは C++マニアック的に考えると、致命的な欠点であるといえます。実は、この問題を回避する方法はあるのですが、これは、クラスオブジェクトとして作成する方法の後半で解説します。
引数のあるマニピュレータで問題になるのは、どのように引数を渡すのかということですが、引数というからには、関数の引数として渡す以外に方法はありません。問題は、どのような関数をどのように使うかということですが、これには次のような方法があります。
マニピュレータを、
最後の方法がもっとも単純明快でお勧めです。これについては、クラスオブジェクトとして作成する方法で紹介しますので、今すぐそちらに飛んでいただいてもかまいません。グローバル関数として作成する方法は、関数をクラスの friend として定義することにより、ヘッダの中で定義可能です。グローバルな関数オブジェクトとして作成する方法は、既に説明したのと同様の理由でヘッダ内で定義することが現実的に不可能という問題があり、あまり実用性はありません。この二つは、こんなやり方もあるということを知っていただくために、解説します。
次のようにint 型の引数を取るマニピュレータを作成するとします。動作は、単純に、int 型引数の値を表示するだけとします。int 型変数は、そのまま cout に挿入できるので、この動作自体は、馬鹿げていますが、ただでさえややこしいマニピュレータの解説にはこのぐらいが適しています。
cout << IntManiac(10);
この方法の場合、まず、マニピュレータをグローバル関数として次のように定義します。つまりこの方法では、マニピュレータそのものはグローバル関数です。
// マニピュレータとなるグローバル関数 IntManiac CIntManiac IntManiac(int iValue) { return CIntManiac(iValue); // コンストラクタに iValue を渡している。 }
このマニピュレータ IntManiac は int 型の引数 iValue を受けて、CIntManiac 型のオブジェクトを返しています。中で行っていることは CIntMniac のコンストラクタを呼ぶことだけです。次に、この IntManiac が返す CIntManiac オブジェクトを受け入れる挿入演算子を次のように定義します。これは、標準ライブラリの外で定義するため ostream のメンバ関数には出来ないので、グローバル関数として定義します。
// CIntManiac を受け入れる挿入演算子 << の定義 ostream& operator <<(ostream& ros, CIntManiac intmaniac) { return intmaniac(ros); }
さて、準備が整ったので、次にクラス CIntManiac を作成します。最初に定義したグローバル関数 IntManiac の中で、CIntManiac オブジェクトのコンストラクタに iValue を渡していますが、これに対応するようにコンストラクタを定義します。また、挿入演算子 << の中で、intmaniac の関数呼び出し演算子に ros を渡しているので、これにも対応するように定義します。ほぼ、次のようになります。
class CIntManiac { private: int m_iValue; // マニピュレータの引数を一時的に保存する変数。 public: CIntManiac(int iValue) { m_iValue = iValue; } ostream& operator()(ostream& ros) { ros << m_iValue; return ros; } };
これで、必要な定義は全て終わったので、このままでも動くのですが、マニピュレータ IntManiac 関連の定義を一つのクラスの定義にまとめたほうがスマートであるし、friend 関数を使うことによって全体をヘッダに入れたときに関数の多重定義のリンクエラーを回避できるという利点があります。そこで、グローバル関数である IntManiac と << を CIntManiac クラスの frined 関数として、全体を一つのクラスの定義に入れてしまいます。結果は、次のようになります。
class CIntManiac { // CIntManiac を受け入れる挿入演算子 << の定義 friend ostream& operator <<(ostream& ros, CIntManiac intmaniac) { return intmaniac(ros); } // マニピュレータとなるグローバル関数 IntManiac friend CIntManiac IntManiac(int iValue) { return CIntManiac(iValue); // コンストラクタに iValue を渡している。 } private: int m_iValue; // マニピュレータの引数を一時的に保存する変数。 public: CIntManiac(int iValue) { m_iValue = iValue; } ostream& operator()(ostream& ros) { ros << m_iValue; return ros; } };
引数のあるマニピュレータの定義サンプル ManipWithParam1.cpp
では、動きを追って見ましょう。
cout << IntManiac(10);
を実行すると、まず、friend 関数にした IniManiac(10) が実行され、その中で CIntManiac のコンストラクタに iValue が渡されます。CIntManiac のコンストラクタでは、iValue を内部の m_iValue にコピーします。コンストラクタが戻ると IntManiac は構築された CIntManiac オブジェクトを返します。最初の式に戻って、このオブジェクトは、frind 関数である演算子 << の引数として渡され、<< は、定義に従って、intmaniac の関数演算子 () に ros つまり、この場合 cout を渡します。関数演算子 () は cout に m_iValue を挿入し、関数が cout を返すと、<< 演算子は同じ cout を返し、元の式全体が cout となり、実行はめでたく終了します。結果的に、iValue と同じ値が cout に挿入されます。
動作は、少し複雑ですが、引数のあるマニピュレータとしての目的は達成されていますね。
引数のあるマニピュレータのもう一つの定義の方法について解説します。
次のようにint 型の引数を取るマニピュレータを作成するとします。動作は、単純に、int 型引数の値を表示するだけとします。前例とまったく同じです。
cout << IntManiac(10);
この方法の場合、グローバルな関数オブジェクトを次のように定義します。この方法の場合、マニピュレータそのものはグローバルな関数オブジェクトです。
class CIntManiac { // CIntManiac を受け入れる挿入演算子 << の定義 friend ostream& operator <<(ostream& ros, CIntManiac intmaniac) { return intmaniac(ros); } private: int m_iValue; public: CIntManiac& operator ()(int iValue) { m_iValue = iValue; return *this; } private: ostream& operator ()(ostream& ros) { ros << m_iValue; return ros; } } IntManiac; // グローバルな関数オブジェクト
引数のあるマニピュレータの定義サンプル ManipWithParam2.cpp
この定義では、マニピュレータは CIntManiac 型のグローバルなオブジェクト IntManiac になっています。動きを追って見ましょう。プログラムが実行されると、グローバルなオブジェクト IntManiac の public 側の関数演算子 () が呼ばれて、iValue が m_iValue にコピーされます。この関数演算子の戻り値は IntManiac オブジェクトそのものであり、これが演算子 << の引数として渡され、intmaniac(cout) の呼び出しが行われます。これは、private 側の関数演算子呼び出しとなり、この関数の中で m_iValue が cout に挿入されます。戻り値は cout で << も同じ cout を返すので、元の式全体が cout となり、実行がめでたく終了します。結果的に iValue の値が cout に挿入されることになります。
マニピュレータとしての動作はちゃんとしていますね。
上記のマニピュレータの定義はグローバルなクラスオブジェクトを実体とするため既に述べたのと同様の理由で、ヘッダに入れることが出来ないという欠点があります。
ここでは、クラスオブジェクトのコンストラクタにマニピュレータの引数を渡す形のマニピュレータの作り方について解説します。この方法はコードサイズが小さく、引数がない場合にも陽に () をつければ使えるので、最も汎用性が高い方法であるといえます。一番良いのは、ヘッダの中で定義しても、まったく問題がないことです。
次のように int 型の引数を取るマニピュレータを作成するとします。動作は、単純に、int 型引数の値を表示するだけとします。前例とまったく同じです。
cout << IntManiac(10);
セナ : これって、結果的には cout << 10; ってことだよネ。
店主 : ウーン、頭の好い娘は、かなわんナ。でも、グローバル関数として作成する方法の冒頭を読んでないということがバレバレですな。
セナ : えっ、えっ、えっ、そうだったの。ヤバッ。
さっそく、マニピュレータを定義してみましょう。私は、通常、クラスの名前は C で始めることにしているのですが、マニピュレータの名前なので、クラス名を特別に IntManic としてあります。つまり、この方法では、マニピュレータそのものがクラスオブジェクトでできているわけです。
class IntManiac { // IntManiac を受け入れる挿入演算子 << の定義 friend ostream& operator <<(ostream& ros, IntManiac intmaniac) { return intmaniac(ros); } private: int m_iValue; public: IntManiac(int iValue) { m_iValue = iValue; } private: ostream& operator()(ostream& ros) { return ros << m_iValue; } };
引数のあるマニピュレータの定義サンプル ManipWithParam3.cpp
動きを追って見ましょう。プログラムが実行されると、IntManiac(10) の呼び出しで、クラス IntManiac のコンストラクタに iValue = 10 が渡されます。コンストラクタは、iValue を m_iValue にコピーします。コンストラクタが戻ると、構築された IntManiac オブジェクトが friend 関数である演算子 << に渡されます。この演算子 << の関数の中では、intmaniac に ros つまりこの場合には cout が渡され、その結果 private な関数演算子 () が呼び出されます。関数 () の中で cout に m_iValue が挿入されます。関数 () は cout を返し、その結果として 演算子 << も同じ cout を返し、元の式全体が cout になって、実行はめでたく終了します。
上記で、引数のあるマニピュレータの機能が実現されていることが分かると思います。
上記の定義は、引数のない場合には、次のように簡略化されます。機能や名前は引数のないマニピュレータの作り方で作成したものと同じものにしてあります。
class HelloManiac { // HelloManiac を受け入れる挿入演算子 << の定義 friend ostream& operator <<(ostream& ros, HelloManiac manipulator) { return manipulator(ros); } privete: ostream& operator()(ostream& ros) { ros << "Hello maniac"; return ros; } };
引数のないマニピュレータの定義サンプル Manip2.cpp
実体が関数であるマニピュレータとは異なり、上記は、クラスオブジェクトでできたマニピュレータなので、使用時には、引数が無いにもかかわらず、コンストラクタを呼ばなくてはならないので () を陽に付ける必要があります。
cout << HelloManiac();
私は、() 付きのマニピュレータも、クラスオブジェクトであることがはっきりして、これはこれで良いのではないかと感じています。しかし、これでは、マニピュレータらしくないという人もいるでしょう。
その場合、基本的には、引数のないマニピュレータの作り方で解説した方法を使うしかありませんが、この方法で作成したマニピュレータはヘッダに入れられないという欠点を持っています。これを回避する方法を考えて見ましょう。実は、回避方法は既にグローバル関数として作成する方法で使った friend 関数を使う方法です。次のように、グローバル関数を囲むクラスを作成します。
class CHelloManiac { friend ostream& HelloManiac(ostream& ros) { ros << "Hello maniac"; return ros; } };
ちょっとインチキっぽいですが、これで、ヘッダの中で定義しても、リンクエラーになることはありません。しかも、() 無しで、次のように呼び出せます。
cout << HelloManiac;
C++マニアックオリジナルの、printf と同じ機能をストリームで使うためのマニピュレータ mprintf をアップしておきます。一つのヘッダの中にきれいに納まったマニピュレータ定義の良いサンプルになっていると思います。ご自由にお使いください。
次のような使い方が、可能です。これ以上の解説は不要でしょう。
cout << mprintf("i = %d", 123);
なお、mprintf の m は maniac の m ではなく、manipulator の m です。もしも maniac の mani であるならば、maniaprintf とするところです。なお、これは、ジョークです。