演算子とマニピュレーター
 
(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
 
 << と >> のオーバーロード ( #18 )
 
 std::strstream などの継承元になっている std::istream は >> の演算子を、 std::ostream は << の演算子を(そして std::iostream は両方の演算子を)持っています。これら「演算子のオーバーロード関数」が int から char * まで様々な型を受け取れるのは、その種類の数だけ関数を持っているからです。
 ということは << や >> に「自分の作ったクラス」を渡すことはできないということです。そのクラスを受け付けるオーバーロード関数が用意されていないからです。
 そこで「自分の作ったクラス用の << や >> を作る方法」を紹介しましょう。なければ、新しく作ってしまえばいいのです。と言ってもメンバ関数として作るのではなく、普通の関数として作るのです。
 
////////////////////////////////////////////////////////////////
//  演算子のオーバーロード関数の作成例。
#include <stdio.h>
#include <iostream>
#include <strstream>

class CInt
{
public:
    int m_i;
};

// 上のクラスを使うための << 演算子のオーバーロード関数。
std::ostream & operator <<
                ( std::ostream &p_rcLOStrm    // cStrStrm
                , const CInt &p_rcRInt )    // cInt
{
    return p_rcLOStrm << p_rcRInt.m_i;    // 既存の関数を呼び出します。
}

//    使用例。
void Use_operator()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out );
    CInt cInt;
    cInt.m_i = 100;

    cStrStrm
        << cInt    // 上の関数が呼び出されます。
        << std::endl << std::ends;
    printf( "%s", ch );
}

// 結果
100

/////////////////////////////
	
 
 演算子のオーバーロード関数は、なにもメンバ関数だけのものではありません。このように普通の関数として作ることもできます。
 引数はメンバ関数のものとちょっと違って、ふたつ受け取ります。これは << の「左側」と「右側」がそのまま引数として渡されます。
 
<<の中身  左側の引数には「 std::ostream への参照」を受け取ります。これが std::strstream でないのは、このようにすることで「すべての出力用ストリームクラスで使用できる」からです。
 右側の引数には、新しく作ったクラスの参照型を渡します。ここでも参照を使っています。参照を使用することで「関数は単なる仲介役」に徹することができます。「演算子のオーバーロード関数」は当然「演算子」を使用します。そして演算子を使用するということはすなわち「変数の中身を変更する」ということです。つまり演算子のオーバーロード関数は「外にある変数を変更するための関数」ということになります。
 この部分を徹底するため、すべての引数を参照として受け取っているのです。
 
 引数の「左」と「右」は厳密に決められています。「 cInt << cStrStr 」としてもこの関数は呼び出されません。左側に必ず「 std::ostream 系クラス」が来なければならないのです。
 でも、ストリーム系クラスは必ず一番左側に持ってくることになっていて、 << は左から順に評価する決まりになっているので「左に std::ostream を、右に新しいクラスを」という関数ひとつ作れば問題なく動作します。
 
 戻り値は「 std::ostream への参照」です。これは「変数に連続して適用する」ためのものです。こうすることで << のチェーンを作ることができます。この参照は左側の引数として渡されたものを返しています。つまり、実際には外にある変数への参照をそのまま返しているということになります。
 
 で、実際にしてることは、単に外にある「 std::ostream 系クラス」を使っているだけです。自分で作ったクラスのメンバ変数を操作して「 std::ostream 系クラス」に渡して書き込んでもらえばいいんです。でもそれが基本です。思いっきり特殊なデータでも、「 std::ostream 系クラス」に渡せる型に変換できれば、出力できるでしょう。
 
 このように、演算子のオーバーロード関数は単なる「仲介役」です。外にあるストリーム系クラスへの参照を通して、新しいクラスのデータをオーバーロード関数へと渡し、そして参照をそのまま返す、それだけの役割です。 しかしそのおかげでストリームクラスを問わずに、そしてこれまでと同じ操作方法で、自分の作ったクラスをストリームクラスへと渡せるようになります。これこそが iostream の真骨頂と言えるでしょう。
 
透明
透明
■ C++ : グローバルな演算子のオーバーロード関数 ( #18 )
 演算子のオーバーロード関数は、グローバル関数としても作ることができます。ただし、いくつかの制限があります。
 
  = 演算子(代入演算子)は、グローバル関数としては作れません。必ず「代入先クラスのメンバ関数」として作る必要があります。
 
 関数の引数には「クラス型」が入っている必要があります。たとえば、次のような演算子関数は作れません。
 
const char *operator +( const char *p_pchLh, const char *p_pchRh )
{
    // 結合などしてから……
    return p_pchLh;
}
	
 
 引数がふたつとも const char * 型、これは組込型なのでこのような演算子関数は作ることができません。ま、要するに「もともとあるものは作れない」ということです。
 
 演算子関数は、基本的に「メンバ変数の書き換え」を行います。ですが、通常メンバ変数は private にするため、書き換えられません。そこで、演算子関数に「書き換えを許可する」ため、 friend 指定を行います。
 
class CWithOutOperator
{
    int m_i;
public:
    CWithOutOperator()
        : m_i( 0 )
    {}

    friend CWithOutOperator operator +
                ( int p_iLh 
                , const CWithOutOperator &p_rcRh );    // friend 指定。
};

// friend 指定された関数。
CWithOutOperator operator +
        ( int p_iLh
        , const CWithOutOperator &p_rcRh )
{
    CWithOutOperator cRet;
    cRet.m_i = p_rcRh.m_i + p_iLh;
    return cRet;
}
	
 
 friend 指定は危険なので、この目的以外には使わないようにしましょう。
 
 演算子関数は、クラス型変数を参照で受け取って、同じ型(非参照)で返すのが普通です。そして返すのは新たに作ったクラス型変数にします。そうすることで演算子に掛けられた変数に影響が及ばないようにします。ただしこれは場合によります。 #15 の時のような演算子のチェーンを行う場合には、戻り値も参照にして、外の変数への参照をそのまま返すようにします。
 
 グローバルな演算子のオーバーロード関数は、このように制約がたくさんあります。グローバル関数として作らなくてはならないのは「自クラスを右項に取る2項演算子」だけです。つまり上の + のような例です。通常は可読性を考えて、このタイプの2項演算子は「自クラスが左項」のものもグローバル関数として作るので、基本的にはグローバル関数として作る必要のある演算子は + - * / の演算子くらいです。それ以外は、メンバ関数として作るようにしましょう。
透明
透明
 
 ちなみにこの例では「書き込み」をしましたが、「読み取り」用に自分のクラスを使いたい場合には「 >> 」の演算子のオーバーロード関数を作って、引数や戻り値を std::istream にしてください。
 
 マニピュレーターを作る ( #19 )
 
 今度は「マニピュレーター」を作ってみましょう。
  std::endl std::setiosflags() などのマニピュレーターは、クラスの状態を変えたり、特殊な文字を渡したり、バッファリングなどの操作を行ったりするために使用します。意外にも、マニピュレーターを作るだけでたいがいのことはできてしまうでしょう。
 まず std::endl のような「引数のないマニピュレーター」と、 std::setiosflags() のような「引数のあるマニピュレーター」とでは、仕組みなどがまったく違うので、別々に解説します。
 「引数のない方」は、比較的簡単に実現できます。まずはこちらから。
 
 フォーマットクラスには << と >> という演算子のオーバーロード関数がメンバ関数として備わっています。その中に、こんな変なものを受け取るオーバーロード関数があります。
 
    ostream& operator<<( ostream& ( *pfunc )( ostream& ) );
	
 
[注:引数の部分が "__omanip" のようなものに置き換わっている場合がありますが、同じことです]
 
 このオーバーロード関数は「関数ポインタ」を受け取ります。そして、これこそが std::endl や std::ends を受け取るオーバーロード関数なのです。 #02 でこのふたつが関数だと紹介しました。これは、この仕組みのためなのです。
 つまり「引数のないマニピュレーター」は std::endl と同型式の関数として作ればいいわけです。これは次のような引数と戻り値を持つ関数です。
 
    std::ostream &Manipulator( std::ostream &p_cOstream )
	
 
マニピュレーターが呼ばれます  << 関数の中では、渡された関数ポインタを通してこのマニピュレーターが呼び出されます。そのとき << の左項にあるストリームクラスへの参照が引数として渡されるので、それに対して操作すればいいのです。 std::endl の中ではストリームクラスに改行コード( '\n' )を送り、さらにバッファリングするのです。
 では実際にこの形式でマニピュレーターを作って使用してみましょう。
 
////////////////////////////////////////////////////////////////
//  引数のないマニピュレーター。
#include <stdio.h>
#include <iostream>
#include <strstream>

std::ostream &Manipulator( std::ostream &p_cOStrm )
{
    return p_cOStrm << "In manipulator.";
}

//    使用例。
void Use_Manipulator()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out );

    cStrStrm
        << Manipulator    // マニピュレーターを渡しました
        << std::endl << std::ends;
    printf( "%s", ch );
}

// 結果
In manipulator.

/////////////////////////////
	
 
 このように「戻り値と引数が std::ostream への参照」の関数を作れば、それが「引数のないマニピュレーター」として使えます。これをそのまま(小カッコを付けずに)渡せば、フォーマットクラスのオーバーロード関数が関数ポインタを通してマニピュレーターを呼び出してくれます。
 呼び出されたら << の左項にあるストリームクラスへの参照(上の例なら cStrStrm )が引数として渡されるので、その引数に対して操作します。操作が終わったら、その引数をそのまま戻り値として返します。
 
 注目して欲しいことは、このマニピュレーター関数が前回紹介した「自作クラス用オーバーロード関数」に似ているという点です。
 
std::ostream & operator <<
                ( std::ostream &p_rcLOStrm    // cStrStrm
                , const CInt &p_rcRInt )    // cInt
{
    return p_rcLOStrm << p_rcRInt.m_i;    // 既存の関数を呼び出します。
}
	
 
 基本的に関数としての機能や使い道は異なります。でも、戻り値や引数、そして実装方法が似ています。また、使い道としても「直接関数として呼び出さない」という点が似ています。
 これが「ストリームクラスの設計思想」です。演算子のオーバーロード関数もマニピュレーターも、外の変数の仲介役としてのみ働きます。また、演算子や変数のように振る舞わせることで、何気なく関数を呼び出すことに成功しています。
 
  std::endl は普通の関数ですが、実際にはそう呼び出さず、変数のように渡すことで間接的に呼び出されます。それはもちろん、 << などと連携を組み、分かりやすく操作できるようにしたかったからです。そのために、関数ポインタという難しい仕組みをわざわざ利用しているのです。
 
 引数付きマニピュレーターを作る ( #20 )
 
 実は「引数のあるマニピュレーター」は困ったことになっています。それは「ライブラリによって実装方法が違う」からです。
 ここまで紹介してきた機能やプログラムは、いくつかのコンパイラでも通ることを確認しています。ところが、「引数のあるマニピュレーター」はそういかないのです。たとえば std::setiosflags() の真似をした場合、そのプログラムは他のコンパイラには通らないかもしれません。
 ここでは、「引数のあるマニピュレーター」の基本的な仕組みについてのみ解説します。では、見てみましょう。
 
////////////////////////////////////////////////////////////////
//  引数のあるマニピュレーター。
#include <stdio.h>
#include <iostream>
#include <strstream>

// 仲介役クラスです。
class CPlusManip1Para
{
    int m_i;
public:
    CPlusManip1Para( int p_i )
        : m_i( p_i )
    {}

    virtual void Do( std::ostream &p_rcLOStrm ) const
    {
        p_rcLOStrm << m_i;
    }
};

// マニピュレーター。
CPlusManip1Para Manipulator1Para( int p_i )
{
    return CPlusManip1Para( p_i );
}

// マニピュレーター用の、演算子のオーバーロード関数です。
std::ostream & operator << 
        ( std::ostream &p_rcLOStrm
        , const CPlusManip1Para &p_rcRManip )
{
    p_rcRManip.Do( p_rcLOStrm );
    return p_rcLOStrm;
}

//    使用例。
void UseManip1Param()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out );

    cStrStrm
        << Manipulator1Para( 100 )    // マニピュレーターを使用します。
        << std::endl << std::ends;
    printf( "%s", ch );
}

// 結果
100

/////////////////////////////
	
 
引数付きマニピュレーター  かなり複雑ですね。実際に iostream でなされている実装はさらに複雑なんで、とりあえずこれには慣れてください。
 まず作るのが、 << 演算子の関数です。 << の「新しく作るクラス(ここでは CPlusManip1Para )」のオーバーロード関数を作ります。そして、この中で、このクラスへの操作を行うようにします。
 次にその「新しく作るクラス( CPlusManip1Para )」を作ります。このクラスには、先ほどの << 演算子で呼び出す関数を持たせます。また、引数付きコンストラクタも持たせます。このコンストラクタが「引数付きマニピュレーター」の実質的な引数となります。
 最後に、このクラスの変数を返す関数を作ります。この関数が、マニピュレーターになります。マニピュレーターに引数を持たせて、返す「新しいクラス」型の変数のコンストラクタに渡します。
 
 鍵は「クラス」です。この例なら CPlusManip1Para の派生クラスを作り Do() をオーバーライドして、その中で特殊な処理をさせます。こうして作った派生クラスの数だけ、マニピュレーターを作成します。
 つまり << 演算子の関数とベースクラスはひとつだけにして、派生クラスとそれを作るマニピュレーターを作っていくわけです。 << とベースクラスはインターフェイス、派生クラスは実際の操作、マニピュレーターは特定の派生クラスを指定しての生成、と役割分担するわけですね。
 
 ちなみに実際のライブラリでは、仮想関数ではなく関数ポインタを用いて汎用性を持たせています。つまりクラスの派生はせず、関数を作ってそのポインタをマニピュレーターが使い分ける形になっています。
 ライブラリの違いというのは「ベースクラス」の部分です。そのため、あるライブラリのベースクラスに合わせてクラスとマニピュレーターを作っても、それは他のライブラリでは << 演算子に使うことができないのです。
 皆さんが「引数付きマニピュレーター」を作る場合には、このように新たにインターフェイスを作ってしまうか、あるライブラリ専用のものを作り、他のライブラリ用にインターフェイス( << や >> )を作るか、という方法を取るのがいいでしょう。
(C)KAB-studio 2001 ALL RIGHTS RESERVED.