LoginSignup
1
1

(C++)独自の streambuf を実装する

Last updated at Posted at 2023-08-06

点群表示機能を自作してみるのおまけです。
std::streambuf の派生クラスの作り方のメモです。

背景

概要

C++プログラム中で必要となるバイト列のIOを抽象化するインタフェースとして streambuf を再利用しようという試みです。

std::streambuf は std::istream, std::ostream の入力元・出力先となる抽象クラスです。 通常 stringstream, ifstream などが直接使われるため、streambuf をユーザーが直接使用する機会は多くありませんが、裏では stringbuf, filebuf といった streambuf の派生クラスが使われています。

今回この streambuf をバイト列のIOを抽象化するインタフェースとして用いることを考えます。 Java でいうところの java.io.InputStream, java.io.OutputStream に相当するものとして streambuf を使おうということです。

今回はバイトストリームとして用いることがメインですので streambuf に限定します。 basic_streambuf については扱いませんが拡張方法は同じです。 (wchar_t が32bitなシステムで eof() とかどう扱うのか少々気になりますが。)

streambuf を拡張する動機

streambuf を用いることのメリットとデメリットは以下位でしょうか。(想定代替案は独自インタフェース作成。)

  • メリット
    • C++ 標準である。 istream, ostream と結合して使える。
    • 自身のコードが streambuf を用いることで、標準の std::filebuf などを使えるようになる。(単体テストでは便利な代替となる。)
    • バッファリング機能を簡単に実装できる。 (バッファリングのI/Oの実装が完全には分離していないので一部考慮した実装が必要ですが。)
  • デメリット
    • インタフェース(メンバ関数名)が分かりにくい。
      • これは streambuf を使うためのラッパを用意してあげれば済むと考えます。
    • 読み込みと書き込みが分離していない。 そのため仕様が複雑になる。
      • 必要ならば片方だけを実装した streambuf クラスを作れば実際上分離できます。
      • ちなみにファイルのIOを想定する限りでは、分離していないことはむしろメリットだと思います。

記事化の動機

streambuf については名前がおかしい(古い?)以外はよくできていると思うのですが、その名前のおかしさからか拡張方法のドキュメントが十分に存在しないように思います。
多いのは overflow()/underflow() をオーバーライドすれば動く、というものです。 これは真実ですしそれ自体が streambuf の優れたところの一つであろうと思うのですが、1バイトずつ仮想関数を呼ぶような動作になっては性能上の足かせが大きすぎます。
streambuf には1バイトを扱う仮想関数の他、複数バイトをまとめて扱う仮想関数も存在します。今回はそれらもオーバーライドしたいと思います。

実装

サンプルコードとしては D3DWin32FileStreamBuf を参照してください。 ファイルハンドルをクローズする RAII クラスになっていないのは見逃してください。

以下GitHubの以下のリビジョンを元に説明します。

Revision: c4d01ce909efef89450ac6691467ae97ec23f5d3
Message:
fixed some potential bugs of D3DWin32FileStreamBuf.

  • fixed a bug of xsputn().
  • fixed some warnings.
  • implmented underflow() in the case of no buffer.
  • fixed a bug in case of big data IO.
  • fixed a bug of destructor.

D3DWin32FileStreamBuf Win32 のファイル(CreateFile() で作成したファイルハンドル)に対して IO する streambuf です。 内部的なバッファとして std::vector<char> m_buffer を利用してIOします。(外部からのバッファの設定には未対応。)

なお以下のコードでは分かりやすさのためエラー処理を一部 assert に置き換えています。ご了承ください。(実際のコードでは例外を投げているところも assert に置き換えている。)

読み込み

必要な処理はバッファ用ポインタの初期化と、いくつかの protected 仮想関数の実装です。

読み込み用初期化
    setg(nullptr, nullptr, nullptr);
読み込み実装用仮想関数
protected:
	virtual std::streamsize xsgetn(char_type* aBuffer, std::streamsize bufCount);
	virtual int_type underflow();
	virtual int_type uflow();

このあたりの関数名の意味不明さが streambuf のデメリットです。
「g ~ get ~ read 用関数」位の連想であろうと思います。

xsgetn(), underflow(), uflow() の実装は以下のような感じでよいと思います。
ReadFromFile() メンバ関数が実質的な実装を処理します。引数で渡された char 配列にファイルから読みこみます。 この実装を変えれば自在に拡張できるでしょう。

読み込み用仮想関数の実装
std::streamsize D3DWin32FileStreamBuf::xsgetn(char_type* aOutByte, std::streamsize nOutByte)
{
	streamsize streamBufferSize = static_cast<streamsize>(m_buffer.size());
	streamsize nCopied = 0;
	while (nCopied < nOutByte) {
		streamsize nRequiredByte = nOutByte - nCopied;
		streamsize nByteInBuf = egptr() - gptr();
		if (0 < nByteInBuf) {
			// Copy data from buffer.
			streamsize nCopy = min(nByteInBuf, nRequiredByte);
			if (INT_MAX < nCopy) {
				// gbump() supports only int. So maximum number here shall be INT_MAX.
				nCopy = INT_MAX;
			}
			traits_type::copy(aOutByte + nCopied, gptr(), (int)nCopy);
			gbump((int)nCopy);
			nCopied += nCopy;
		}
		else {
			if (nRequiredByte < streamBufferSize) {
				// Read to the buffer and copy from buffer at the next turn.
				if (traits_type::eq_int_type(underflow(), traits_type::eof())) {
					// undeflow() didn't work.
					break;
				}
			}
			else {
				// Read to the output buffer directly.
				streamsize nCopy = ReadFromFile(nRequiredByte, aOutByte + nCopied);
				nCopied += nCopy;
				if (nCopy < nRequiredByte) {
					// If data is not read enough, return from this function call.
					break;
				}
			}
		}
	}
	return nCopied;
}

D3DWin32FileStreamBuf::int_type D3DWin32FileStreamBuf::underflow()
{
	// This function may enable input buffer. So output buffer must have been disabled.
	assert(pbase() == nullptr);
	assert(gptr() == egptr());
	if (m_buffer.empty()) {
		// sgetc() may call underflow() in this case.
		// Add a buffer for single character to hold current position.
		// In any case, small buffer is not used by xsgetn() or xsputn().
		m_buffer.resize(1);
	}
	streamsize streamBufferSize = static_cast<streamsize>(m_buffer.size());
	streamsize nRead = ReadFromFile(streamBufferSize, m_buffer.data());
	if (nRead == 0) {
		return traits_type::eof();
	}
	setg(m_buffer.data(), m_buffer.data(), m_buffer.data() + nRead);
	return traits_type::to_int_type(*gptr());
}

D3DWin32FileStreamBuf::int_type D3DWin32FileStreamBuf::uflow()
{
	if (m_buffer.empty()) {
		// implementation for sbumpc() in the case of no buffer.
		assert(gptr() == egptr());
		char_type nextByte = 0;
		streamsize nRead = ReadFromFile(1, &nextByte);
		if (nRead == 0) {
			return traits_type::eof();
		}
		return traits_type::to_int_type(nextByte);
	}
	else {
		// Use implementation of the base class which uses underflow().
		return streambuf::uflow();
	}
}

蛇足ながら

  • xsgetn() は public 関数 sgetn() の実装で、バイト列を読み込む入り口となる関数です。
    • streambuf の読み込みバッファも考慮した実装が必要です。
    • xsgetn() を実装しないと、 streambuf の読み込みバッファを無効化した場合に 1 バイトずつ uflow() で読み込む実装になります。 これが嫌だったので自前の実装を足しています。
  • underflow() は streambuf が持つ読み込みバッファに元になるファイルから読み込む関数です。 この streambuf が指すバイト位置は変えません。
    • バッファが空になっていることを前提に実装します。(少なくとも VisualStudio 付属の STL の実装では。)
    • 仕様上 streambuf の読み込みバッファが有効(空であれ、設定されている)でないと動作しない関数です。
  • uflow() は元になるファイルから1バイト読んで返す関数です。同時に streambuf が指すバイト位置も進めます。
    • streambuf の読み込みバッファが有効であれば、underflow() を用いて実装することが可能であり、実際そのような実装が streambuf::uflow() にあります。
    • ここでは読み込みバッファを使わない場合の実装がメインになります。

書き込み

必要な処理は読み込みと同じく、バッファ用ポインタの初期化といくつかの protected 仮想関数の実装です。

書き込み用初期化
    setp(nullptr, nullptr);

こちらは「p ~ put ~ write用関数」位の連想でしょう。

書き込み実装用仮想関数
protected:
	virtual std::streamsize xsputn(const char_type* aBuffer, std::streamsize bufCount);
	virtual int sync();
	virtual int_type overflow(int_type nextValue);

xsputn(), sync(), overflow() の実装は以下の通りです。実質的なファイル出力の実装は WriteToFile() メンバ関数で行われます。 引数で渡された配列を出力します。

書き込み用仮想関数の実装
std::streamsize D3DWin32FileStreamBuf::xsputn(const char_type* aBuffer, std::streamsize bufCount)
{
	// This function may enable output buffer. So input buffer must have been disabled.
	assert(eback() == nullptr);

	if (pbase() == epptr() && !m_buffer.empty()) {
		// enable buffer
		setp(m_buffer.data(), m_buffer.data() + m_buffer.size());
	}

	if (pbase() < pptr() && pptr() == epptr()) {
		// There are some bytes in the buffer and the buffer is full.
		// Write the buffer to file in order to try to make some space in the buffer.
		streamsize nByteInBuf = pptr() - pbase();
		assert(nByteInBuf <= SIZE_MAX);
		streamsize nWritten = WriteToFile(nByteInBuf, pbase());
		assert(0 <= nWritten);
		if (nWritten == 0) {
			return 0;	// failed to write.
		}
		if (nWritten < nByteInBuf) {
			size_t nRemainingByte = static_cast<size_t>(nByteInBuf - nWritten);
			traits_type::move(pbase(), pbase() + nWritten, nRemainingByte);
			setp(pbase(), pbase() + nRemainingByte, epptr());
		}
		else {
			setp(pbase(), epptr());
		}
	}

	streamsize bufferSpace = epptr() - pptr();
	if (bufCount <= bufferSpace) {
		// If there are enough space in the buffer, write data to the buffer.
		streamsize i = bufCount;
		streamsize numCopied = 0;
		while (INT_MAX < i) {
			traits_type::copy(pptr(), aBuffer + numCopied, INT_MAX);
			pbump(INT_MAX);
			numCopied += INT_MAX;
			i -= INT_MAX;
		}
		traits_type::copy(pptr(), aBuffer + numCopied, static_cast<int>(i));
		pbump(static_cast<int>(i));
		return bufCount;
	}
	// There is no enough space in the buffer, write buffer data at first, and write aBuffer after that.

	if (pubsync()) {
		return 0;	// pubsync() was failed.
	}

	return WriteToFile(bufCount, aBuffer);
}

int D3DWin32FileStreamBuf::sync()
{
	if (pbase() < pptr()) {
		// write buffer to file.
		streamsize nByteInBuf = pptr() - pbase();
		streamsize nWritten = WriteToFile(nByteInBuf, pbase());
		if (nWritten < nByteInBuf) {
			return -1;	// failed to write.
		}
		setp(pbase(), epptr());
	}
	return 0;
}

D3DWin32FileStreamBuf::int_type D3DWin32FileStreamBuf::overflow(int_type nextValue)
{
	if (!traits_type::eq_int_type(nextValue, traits_type::eof())) {
		char_type c = traits_type::to_char_type(nextValue);
		streamsize nWritten = sputn(&c, 1);
		if (nWritten == 0) {
			return traits_type::eof();
		}
	}
	return traits_type::not_eof(nextValue);
}

こちらも蛇足ながら

  • xsputn() は public 関数 sputn() の実装で、バイト列を書き出す入り口となる関数です。
    • streambuf の書き出しバッファを考慮した実装をする必要があります。読み込みの xsgetn() と対応する関数と言えます。
  • sync() は public 関数 pubsync() の実装で、 streambuf の書き出しバッファに書かれた内容を出力先に書き出す関数です。
  • ovrflow() は書き出しバッファが使えない時に1バイト出力するための関数です。
    • 今回は xsputn() にバッファが使えない時の実装も入れているのでそちらを呼び出しています。
    • Microsoft のドキュメントではバッファが使える時に呼び出してもよさそうなことが書かれてます。)

The overflow function is most frequently called by public streambuf functions like sputc and sputn when the put area is full, but other classes, including the stream classes, can call overflow anytime.

seek

seek 系の関数の実装サンプルです。
seek処理の実体は SeekFile() にあります。ResetBuffer() は展開しておきました。

seek系関数の実装

D3DWin32FileStreamBuf::pos_type D3DWin32FileStreamBuf::seekoff(
	off_type offset, ios_base::seekdir way, ios_base::openmode mode /*= ios_base::in | ios_base::out*/
)
{
	if (pbase() < pptr()) {
		if (pubsync()) {
			assert(false); // pubsync() failed.
		}
	}
	off_type fileOffset = offset;
	if (way == ios_base::cur) {
		if (eback() != nullptr) {
			ptrdiff_t numByteInBuffer = egptr() - gptr();
			P_ASSERT(0 <= numByteInBuffer);
			fileOffset -= numByteInBuffer;
		}
	}
	//ResetBuffer();
    setp(nullptr, nullptr);
    setg(nullptr, nullptr, nullptr);

	return SeekFile(fileOffset, way);
}

D3DWin32FileStreamBuf::pos_type D3DWin32FileStreamBuf::seekpos(
	pos_type pos, ios_base::openmode mode /*= ios_base::in | ios_base::out*/
)
{
	return seekoff(pos, ios_base::beg, mode);
}

なお seekoff(), seekpos() の引数 mode は今回の実装では使用していません。 仕様上どのように動くべきかが実装依存のようです。

読み込み、書き出しバッファの共有について

streambuf は読み込みと書き出しのバッファを別々に保持できますが、今回は共有する実装としています。 そのため読み込みと書き出しを切り替える際には pusbseekoff(), pubseekpos() を呼ぶ必要があります。 これは fstreambuf の動作を参考にした実装です。 (Microsoftのドキュメントより。)

未実装機能

以下の仮想関数は実装していません。

未実装機能
    virtual int_type pbackfail(int_type = _Traits::eof());
    virtual streamsize showmanyc();
    virtual basic_streambuf* setbuf(_Elem*, streamsize);
    virtual void __CLR_OR_THIS_CALL imbue(const locale&);

pbackfail() は入力を戻す機能、showmanyc() はベースとなるストリームから読み取れるバイト数を返す機能で、どちらも無理に実装する必要はないと思い作っていません。

  • pbackfail() は public 関数である sputbackc() や sungetc() の実装ということになりますが、これらの関数はバッファに乗っている範囲では pbackfail() の実装がなくても動作します。

setbuf() は今回バッファは内部で持つ vector で賄うことを前提に作ってしまったので実装できていません。これは今後改善の余地があると思います。

  • ちなみに Visual Studio2019 の basic_filebuf の setbuf() の実装では FILE* に対して setvbuf() を呼んでいました。そういう解釈もありなのですね。

imbue() はロケールの設定ですが、何に使えばよいか分かりません。

課題

その他今回の実装は以下の前提で作っています。場合によっては仕様変更が必要でしょう。

  • 読み込み時に要求されたサイズ読めなかった場合はリトライを粘らず素直に諦める。 (リトライは呼び出し側に頑張ってもらう方針。)

参考資料

  • basic_streambuf (Microsoft)
    • 個々の関数の説明は Microsoft のページが分かりやすいです。
  • streambuf (C++日本語リファレンス)
    • 関数の分類はこちらの方が分かりやすいです。
  • boost::iostream (Let's Boost)
    • boost::iostreams を使えば簡単に stream (streambuf でなく)を実装できるよ、という内容。
  • iostreamの拡張
    • overflow(), underflow() の実装主体で iostream を拡張する話題。
1
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1