std::ostream で独自のバッファーにストリーム出力する - C++ プログラミング

PROGRAM


std::ostream で独自のバッファーにストリーム出力する

C++ では std::streambuf の派生クラスを作ることで、std::ostream を使って任意のバッファーにストリーム出力できます。

C++ の場合は std::ostream の "<<" 演算子を使った処理がなかなか使い勝手がいいので、たとえば今回は char* 型のバッファーに std::ostream を使ってデータを揃えるということをやってみたいと思います。

 

準備

std::ostream や std::streambuf を使うには、次のように <iostream> ヘッダーをインクルードしておく必要があります。

#include <iostream>

 

ストリームバッファーを定義する

例えば、次のバッファーにストリームを使って値を書き込みたいとします。

char* buffer[1000];

ストリームを使って書き込む場合は、書き込みを行う領域を管理する std::streambuf が必要になります。

ただ、この std::streambuf に外からバッファーを割り当てることはできないため、std::streambuf の派生クラスを作成して、外部で確保されたバッファーをアタッチできるトリームバッファー CExternalBuffer を作成します。

 

CExternalBuffer プロトタイプ宣言

class CExternalBuffer : public std::streambuf

{

public:

CExternalBuffer() = delete;

CExternalBuffer(char* buffer, size_t bufferLength);

};

このように、std:: streambuf を継承した CExternalBuffer クラスを作成します。

このクラスを、今回はコンストラクタで外部バッファとその長さを受け取ってインスタンス化できるようにします。

 

CExternalBuffer 実装

CExternalBuffer::CExternalBuffer(char* buffer, size_t bufferLength)

{

char* bufferEnd = buffer + bufferLength;

 

setp(buffer, bufferEnd);

setg(buffer, buffer, bufferEnd);

}

実装では、基底クラスの setp 関数と setg 関数を使って内部バッファの初期化を行っています。

  • setp 関数では、データを書き込むバッファの位置を設定していて、引数では "開始位置" と "終了位置" を指定します。
  • setg 関数では、データを読み込むバッファの位置を設定していて、引数では "開始位置" と "現在位置"、そして "終了位置" を指定しています。

ここで、"終了位置" は、最後の値が格納できる位置の次の位置のことです。

 

単純に外部バッファをストリームで扱えるようにするストリームバッファであれば、これだけの実装で、ストリーム演算子や、read 関数や write 関数、put 関数や get 関数などを使って、ストリームで読み書きできるようになります。

unget 関数によるひとつ前に戻る操作は可能です。

 

ただし、これだけの実装の場合、seekp 関数や seekg 関数を使った自由な移動には対応していません。

ストリームの seekp 関数や seekg 関数での自由な移動に対応させるためには、ストリームバッファで seekpos 関数や seekoff 関数をオーバーライドして実装する必要があります。

これについては 自由に移動できるストリームバッファを作成する で詳しく紹介します。

 

std::streambuf から継承したクラスで使える機能

扱うバッファーを細かく制御をしたい場合には、std::streambuf に実装されているいくつかの関数が使えます。

書き込み位置の制御
pbase() 書き込み位置の開始位置を取得します。
pptr() 現在の書き込み位置を取得します。
epptr()  書き込み位置の終了位置を取得します。
pbump(int) 書き込み位置を、現在位置から、引数で指定された数だけ前後に移動します。
読み込み位置の制御
eback() 読み込み位置の開始位置を取得します。
gptr() 現在の読み込み位置を取得します。
egptr()  読み込み位置の終了位置を取得します。
gbump(int) 読み込み位置を、現在位置から、引数で指定された数だけ前後に移動します。
動作の制御(オーバーライド)
int sync() オーバーライドすると、std::flush が送信されたときの処理を記載できるので、バッファされた内容を処理してクリアしたりします。std::endl や std::ends が送信されたときにも std::flush が実行されます。正常終了の場合は 0 を、異常終了の場合は -1 を返すようにします。
pos_type seekoff(off_type, seekdir, openmode) ストリームの相対位置指定の seekp(off_type, seekdir) 関数が呼び出されたときの処理を記載できます。これを実装しない場合は seekp で相対移動を命令しても無視されます。
pos_type seekpos(pos_type, openmode) ストリームの絶対位置指定の seekp(pos_type) 関数が呼び出されたときの処理を記載できます。これを実装しない場合は seekp で絶対移動すると、有効な位置が取得できなくなります。
int_type overflow(int)  オーバーライドすると、書き込み位置が終了位置に来た時の処理を記載できます。
int_type underflow() オーバーライドすると、読み込み位置が終了位置に来た時の処理を記載できます。

この他にもいろいろな関数が用意されているので、高度なストリームバッファーを作るときには、このような機能を利用して行くことになります。

 

自作のストリームバッファーにストリーム出力する

作成した CExternalBuffer を使って外部バッファにデータを書き込むストリームを用意します。

ストリームは派生クラスを作らなくても、std::iostream に CExternalBuffer(std::streambuf を継承したクラス)の参照を渡してインスタンスを作成すれば、それをバッファとして使ってくれます。

char* buffer[1000];

CExternalBuffer streamBuffer(buffer, 1000);

 

std::iostream stream(&streamBuffer);

これで、ストリームを使って出力できるバッファが完成しました。

あとはこの stream 変数を使って std::cout と同じように "<<" 演算子を使ってストリーム出力することができます。そして、そうして出力したデータは CExternalBuffer クラスが管理する buffer 内に格納されます。

stream << "TEST DATA" << '\0';

なお、今回作成した CExternalBuffer では、受け取ったデータをそのままメモリに書き込むだけなので、strcpy 関数や sprintf 関数を使ったときなどのような、末端を 0 で埋めるようなことはしません。

そのため、ストリーム出力を終えた buffer 変数をそのまま文字列として使おうとすると、テキストの末尾にゴミが入る場合があるので注意します。

出力したバッファを C 文字列として扱いたい場合には、最後にストリームに "<< '\0'" として、NUL 文字を挿入する必要があります。

 

ストリームバッファーとストリームをひとつにまとめる

ストリーム "std::iostream" とストリームバッファ "CExternalBuffer" の両方を構築して扱うのが面倒であれば、さらに std::iostream の派生クラスを作成して、内部に CExternalBuffer を持たせる方法もあります。

std::iostream からの派生クラスの作成もとても簡単で、次のようなプロトタイプ宣言になります。

 

CExternalStream のプロトタイプ宣言

class CExternalStream : public std::iostream

{

private:

CExternalBuffer m_buffer;

 

public:

CExternalStream() = delete;

CExternalStream(char* buffer, size_t bufferLength)

};

このように、メンバ変数として CExternalBuffer クラスを持たせ、コンストラクタでは外部バッファとその長さを受け取るようにします。

実装では、受け取った引数をそのまま使って CExternalStream インスタンスを初期化するだけです。

 

CExternalStream の実装

CExternalStream::CExternalStream(char* buffer, size_t bufferLength) : m_buffer(buffer, bufferLength), std::iostream(&m_buffer)

{

}

このように、受け取った外部バッファの情報を使って CExternalBuffer 型のメンバ変数 m_buffer をコンストラクタで初期化します。

それと合わせて、そのバッファを std::iostream のコンストラクタに参照渡しすれば実装完了です。

 

これで後は、この CExternalStream をインスタンス化して直ぐに使えます。

char* buffer[1000];

CExternalStream stream(buffer, 1000);

ストリームバッファを作成してからそれを使ってストリームを初期化するという部分が簡単になって、ソースコードもずいぶん見通しが良くなりました。


[ もどる ]