パラメータ付きのマニピュレータでは、考慮すべき点が増えるため、パラメータのないマニピュレータよりも複雑です。これらの点を詳しく説明し、パラメータ付きマニピュレータのさまざまな実装方法を検討する前に、1 つ例を挙げます。これは、setprecision() や setw() などの標準マニピュレータを実装する手法です。
Rogue Wave の標準入出力ストリームの実装では、パラメータ付きマニピュレータのほとんどを、ある 1 つの手法で実装しています。マニピュレータ型 manipT は、関数ポインタ型です。マニピュレータオブジェクトはポイントされる関数です。付属関数 fmanipT は 大域関数です。
C++ 標準では、マニピュレータ型を smanip と定義しています。itself 型は実装側の定義なので、これを返す標準マニピュレータがあること以外は公開されていません。Rogue Wave の実装で、smanip はクラステンプレートです。
template<class T> class smanip { public: smanip(ios_base& (*pf)(ios_base&, T), T manarg); };
setprecision() のような標準マニピュレータは、大域関数として実装することができ、smanip<T> 型のオブジェクトを返します。
inline smanip<int> setprecision(int n) { return smanip<int>(sprec, n); }
付属関数 fmanipT は大域関数 sprec です。
inline ios_base& sprec(ios_base& str, int n) { str.precision(n); return str; }
前節では、1 つのパラメータのマニピュレータの実装例によって、入出力ストリームにおける標準マニピュレータの実装手法を紹介しました。これ以外にも、パラメータ付きマニピュレータを実装する方法はあります。この節では、その他の手法を紹介します。いずれの方法でも、1 つのパラメータのマニピュレータについて説明していますが、複数のパラメータがあるマニピュレータにも簡単に応用することができなす。
最初は、順序を追って説明します。まず、パラメータ付きマニピュレータとは何であるかを説明します。
パラメータ付きマニピュレータは、次のような式のストリームに対して挿入や抽出することができるオブジェクトのことです。
cout << Manip(x); cin >> Manip(x);
Manip(x) は、manipT 型のオブジェクトであり、これにシフト演算子を多重定義します。対応する挿入子の例を次に示します。
template <class charT, class Traits> basic ostream<charT,Traits>& operator<< (basic ostream<charT,Traits>& ostr ,const manipT& manip) { // 付属関数 fmanipT を呼び出す。例は以下のとおり。 (*manip.fmanipT )(ostr,manip.argf); return os; }
ここで定義された挿入子では、式 cout << Manip(x); は、上記のシフト演算子、operator<<(cout, Manip(x)) の呼び出しと同じはたらきをします。
付属関数 fmanipT で副作用が作成される場合、マニピュレータでは個別に引数で指定して付属関数を呼び出す必要があります。そのため、シフト演算子内からの呼び出しに使用する引数とともに、付属関数を保存しておく必要があります。
付属関数 fmanipT は、静的関数か大域関数、または manipT 型などのメンバー関数です。
上記の挿入子では、付属関数 fmanipT は静的関数か大域関数であるものとし、引数は 1 つだけとします。一般に、マニピュレータ型 manipT は次のようになります。
template <class FctPtr, class Arg1, class Arg2, > class manipT { public: manipT(FctPtr, Arg1, Arg2, ); private: FctPtr fp_; Arg1 arg1_; Arg2 arg2_; };
ただし、これはマニピュレータの単なる例でしかありません。付属する副作用関数とその引数が、マニピュレータ型の個々のシフト演算子内から呼び出せるものであれば、マニピュレータ型は基本的にどのようにでも定義することができます。この章の後半では、たとえばi、 C++ 標準で定義している smanip などのマニピュレータ型の例も紹介します。これは、標準マニピュレータが返す実装定義の関数型です。詳細は、『標準 C++ クラスライブラリ・リファレンス』を参照してください。
上の例では、多重定義されたシフト演算子に対する引数として与えられたマニピュレータオブジェクトは、Manip(x) で得ることができます。その方法は、次の 3 通りが考えられます。
manipT Manip (X x);
class Manip { public: Manip(X x); };
class M { public: manipT operator()(X x); } Manip;
(1) と (2) の解法は、意味論的には (3) の方法と異なります。(1) では、Manip は関数であり、ユーザーが作成する必要はありません。(2) では、Manip はクラス名であり、名前なし一時オブジェクトがマニピュレータオブジェクトのはたらきをします。しかし (3) の Manip は、ユーザーが明示的に作成するマニピュレータオブジェクトです。したがって、ユーザーは次のコードを書く必要があります。
manipT Manip; cout << Manip(x);
この方法は少し不便です。ユーザーはもう 1 つの名前、すなわちマニピュレータ型 manipT を理解している必要があり、また、マニピュレータオブジェクト Manip を作成する必要があるためです。16 このような理由から、(3) の解法はマニピュレータに状態がある場合に適しています。つまり、挿入のたびに次の行番号を設定するような (ここでは lineno という名前にします)、追加データを格納するマニピュレータです。
(2) の方法で問題となるのは、名前のないオブジェクトを処理できないコンパイラがあることです。
以上 3 つの方法は、付属関数を次の中から選択することができます。付属関数 fmanipT として次のいずれかを選択してください。
これら 3 つの選択肢のうち、マニピュレータとその付属関数をカプセル化することができるため、オブジェクト指向プログラムには (b) の静的メンバー関数の利用が適しています。特に (3) のように、マニピュレータに状態があって、マニピュレータが関数オブジェクトであり、付属関数でマニピュレータの状態にアクセスする必要がある場合に最適です。
(c) の仮想メンバー関数を使用する方法では、マニピュレータの挿入や抽出のたびに仮想関数呼び出しのオーバーヘッドがあります。マニピュレータに状態があり、その状態を付属マニピュレータ関数で変更する必要がある場合に適した方法です。静的メンバー関数では、マニピュレータの静的データにしかアクセスすることができません。しかし、非静的メンバー関数では、オブジェクト固有のデータにアクセスすることができます。
この節では、パラメータ付きマニピュレータの例をいくつか紹介します。例は、マニピュレータ型の (1) から (3) の解法と、付属関数の (a) から (c) の方法を適当に組み合わせたものです。また、標準マニピュレータ setprecision() を使用したさまざまな手法を紹介します。
例 1: 関数ポインタと大域関数。この例では、(1) と (c) を組み合わせています。したがって、次のようになります。
標準入出力ストリームの Rogue Wave の実装では、ほとんどのパラメータ付きマニピュレータを上記の方法で実装しています。第 7.3.2 も参照してください。
例 2: 名前なしオブジェクトと静的メンバー関数。この例では、(2) と (b) を組み合わせています。したがって、次のようになります。
マニピュレータ型 manipT は、入出力ストリームで定義したマニピュレータ型 smanip から派生させることができます。次に、setprecision() のようなマニピュレータの別の実装方法を示します。
class setprecision : public smanip<int> { public: setprecision(int n) : smanip<int>(sprec_, n) { } private: static ios_base& sprec_(ios_base& str, int n) { str.precision(n); return str; } };
例 3: 名前なしオブジェクトと仮想メンバー関数。この例では、(2) と (c) を組み合わせています。したがって、次のようになります。
ここでは、付属関数 fmanipT は、マニピュレータ型 manipT の非静的メンバー関数です。このようなモデルでは、マニピュレータには付属関数 fmanipT までのポインタは格納されませんが、付属関数は純粋な仮想メンバー関数として定義されます。したがって、マニピュレータ型 manipT は抽象クラスとなり、具象マニピュレータ型は、その抽象マニピュレータ型から派生させることができます。付属関数を表す仮想メンバー関数は、これらを利用して実装します。
標準マニピュレータ型 smanip は、実装定義のマニピュレータ型のため、 明らかに新しいマニピュレータ型が必要になります。Rogue Waveの標準 C++ ライブラリでは、仮想メンバー関数ではなく、付属関数までのポインタが格納されています。次に、必要な抽象マニピュレータ型を示します。
template <class Arg, class Ostream> class virtsmanip { public: typedef Arg argument_type; typedef Ostream ostream_type; virtsmanip (Arg a) : arg_(a) { } protected: virtual Ostream& fct_(Ostream&,Arg) const = 0; Arg arg_; friend Ostream& operator<< (Ostream& ostr ,const virtsmanip<Arg,Ostream>& manip); };
virtsmanip 型は、次の点で標準型 smanip とは異なります。
標準マニピュレータ smanip では、ios_base での参照のために関数に対するポインタを要求します。この方法では、意図するかしないかにかかわらず、マニピュレータは常に入力ストリームおよび出力ストリームに適用されます。新しいマニピュレータ型 virtsmanip では、入力ストリームに不用意に適用されることのないマニピュレータを定義することができます。
新しいマニピュレータ型ができたので、マニピュレータ挿入子の新しい多重定義バージョンも必要です。
template <class Arg, class Ostream> Ostream& operator<< (Ostream& ostr, const virtsmanip<Arg,Ostream>& manip) { manip.fct_(ostr,manip.arg_); return ostr; }
これらを前提として、次に setprecision() のようなさらに別の実装方法を紹介します。この場合、setprecision() は、出力ストリーム専用のマニピュレータです。
class setprecision : public virtsmanip<int,basic_ostream<char> > { public: setprecision(argument_type n) : virtsmanip<argument_type,ostream_type>(n) { } protected: ostream_type& fct_(ostream_type& str, argument_type n) const { str.precision(n); return str; } };
例 4: 関数オブジェクトと静的メンバー関数。この例では、(3) と (b) を組み合わせています。したがって、次のようになります。
関数オブジェクトをマニピュレータとして使用するこの解法は、マニピュレータオブジェクトに状態があるという点で先の方法と意味論的に異なります。続けて使用する場合もデータを保存することができます。
別の例を利用して、この手法を紹介します。それは、マニピュレータオブジェクトが管理する一定の文字列を挿入する出力マニピュレータです。このようなマニピュレータには、各行に接頭辞を挿入するなどの用途があります。
Tag<char> change_mark("v1.2 >> "); while ( new_text ) ostr << change_mark << next_line; change_mark(""); while ( old_text) ostr << change_mark << next_line;
ここでは、標準マニピュレータ smanip から Tag マニピュレータを派生させています。smanip の付属関数には、ios_base 参照をパラメータに持つという制約があります。この例では、保存テキストをストリームに挿入するため、ストリームの挿入子が必要になります。ただし、ios_base には挿入子や抽出子はありません。したがって、smanip と同様に、出力ストリームに対する参照をとる付属関数が可能な、新しいマニピュレータ基底型が必要になります。
template <class Ostream, class Arg> class osmanip { public: typedef Ostream ostream_type; typedef Arg argument_type; osmanip(Ostream& (*pf)(Ostream&, Arg), Arg arg) : pf_(pf) , arg_(arg) { ; } protected: Ostream& (*pf_)(Ostream&, Arg); Arg arg_; friend Ostream& operator<< (Ostream& ostr, const osmanip<Ostream,Arg>& manip); };
次に、新しいマニピュレータ型 osmanip の挿入子を定義します。
template <class Ostream, class Arg> Ostream& operator<< (Ostream& ostr,const osmanip<Ostream,Arg>& manip) { (*manip.pf_)(ostr,manip.arg_); return ostr; }
関数オブジェクト型 M を定義します。ここでは Tag と呼びます。
template <class charT> class Tag : public osmanip<basic_ostream<charT>, basic_string<charT> > { public: Tag(argument_type a = "") : osmanip<basic_ostream<charT>, basic_string<charT> > (fct_, a) { } osmanip<ostream_type,argument_type>& operator() (argument_type a) { arg_ = a; return *this; } private: static ostream_type& fct_ (ostream_type& str, argument_type a) { return str << a; } };
このマニピュレータ型の意味論は、先の意味論や標準マニピュレータ setprecision とは異なります。次の例のように、このマニピュレータオブジェクトは、使用前に明示的に作成する必要があります。
Tag<char> change_mark("v1.2 >> "); while ( new_text ) ostr << change_mark << next_line; change_mark(""); while ( old_text) ostr << change_mark << next_line;
この種のマニピュレータは、柔軟性に優れています。上の例では、マニピュレータの作成時にデフォルトテキストが "v1.2 >> " に設定されます。その後は、このマニピュレータをパラメータのないマニピュレータとして使用することで、このテキストが引き続き使用されます。これを、引数をとるマニピュレータとしても使用することができます。その場合は、挿入のたびに異なる引数を指定します。
例 5: 関数オブジェクトと仮想メンバー関数。先の例では、静的メンバー関数を付属関数として使用しました。この場合、付属関数でマニピュレータの状態を変更できないのが欠点となります。変更が必要な場合は、代わりに仮想メンバー関数を使用します。
この最後の例では、余分なデータを保存するマニピュレータを使用します。先に説明した lineno マニピュレータです。このマニピュレータは、挿入のたびに次の行番号を追加します。
LineNo lineno; while (!cout) { cout << lineno << ; }
このマニピュレータは、(3) と (b) パターンで実装します。
マニピュレータオブジェクトには、マニピュレータオブジェクトが作成されるたびに初期化される行番号が組み込まれます。lineno マニピュレータを挿入するたびに、行番号が増分します。
マニピュレータ基底型の場合は、例 3 のマニピュレータ型 osmanip を少し変更したバージョンを使用します。この例の付属関数は、定数メンバー関数ではない可能性があるため、変更が必要です。
template <class Arg, class Ostream> class virtsmanip { public: typedef Arg argument_type; typedef Ostream ostream_type; virtsmanip (Arg a) : arg_(a) { } protected: virtual Ostream& fct_(Ostream&,Arg) = 0; Arg arg_; friend Ostream& operator<< (Ostream& ostr ,virtsmanip<Arg,Ostream>& manip); }; template <class Arg,class Ostream> Ostream& operator<< (Ostream& ostr ,virtsmanip<Arg,Ostream>& manip) { manip.fct_(ostr,manip.arg_); return ostr; }
行番号マニピュレータは、次のように実装することができます。
template <class Ostream> class LineNo : public virtsmanip<int,Ostream > { public: LineNo(argument_type n=0) : virtsmanip<argument_type, ostream_type> (n) { } virtsmanip<argument_type,ostream_type>& operator() (argument_type arg) { arg_ = arg; return *this; } protected: argument_type lno_; ostream_type& fct_ (ostream_type& str, argument_type n) { lno_ = (n>0) ? n : lno_; str << ++lno_; arg_ = -1; return str; } };
仮想メンバー関数を付属マニピュレータ関数にすると、マニピュレータを挿入するたびに、仮想関数呼び出しのオーバーヘッドが実行されます。挿入のたびにマニピュレータの状態を更新する必要がある場合は、静的メンバー関数は適していません。この場合は、マニピュレータ型のフレンドである大域関数が適しています。ただし、オブジェクト指向プログラムでは、フレンド先の非公開データメンバーや保護データメンバーを変更する大域変数は使用しないでください。
Copyright (c) 1998, Rogue Wave Software, Inc.
このマニュアルに関する誤りのご指摘やご質問は、電子メールにてお送りください。
OEM リリース, 1998 年 6 月