meryngii.neta

今日も新たな"ネタ"を求めて。

operatorでストリームの書式状態を保存する

C++のストリームの書式状態は、一度変更すると再び変更しない限りそのままになる。書式フラグを元に戻すのを忘れていれば、同じストリームを使った別の部分のコードで思わぬ表示が出るかもしれない。
書式状態を保存するためのライブラリとしてboost::ios_stateというものがあるが、いちいちスコープが必要で、それぞれのクラス名も覚えなければならないのであまり使いやすくはない。
そこで、operator<と>をオーバーロードして、一文ごとにストリームの書式状態を復元する簡単なライブラリを作ってみた。これを使うとこのように書ける。

cout < hex < 256 < endl;

cout << 256 << endl;

すると次のように表示される。

100
256

普通にすべてシフト演算子で書けば、2つ目もhexが効いて16進数で表示されるはずだが、この例だと1つ目の文が終わると状態が復元されて10進数で表示される。
このライブラリのコードは次の通り。

#include <iostream>

template <class charT, class traits>
class saved_stream
{
public:
	explicit saved_stream(std::basic_ios<charT, traits>& stream)
		: stream_(stream), is_last_(false)
		, flags_(stream.flags()), fill_(stream.fill()), precision_(stream.precision()), width_(stream.width()) { }
	
	saved_stream(const saved_stream& saved)
		: stream_(saved.stream_), is_last_(true)
		, flags_(saved.flags_), fill_(saved.fill_), precision_(saved.precision_), width_(saved.width_)
	{
		saved.is_last_ = false;
	}
	
	~saved_stream()
	{
		if (is_last_)
		{
			stream_.flags(flags_);
			stream_.precision(precision_);
			stream_.width(width_);
			stream_.fill(fill_);
		}
	}
	
private:
	std::basic_ios<charT, traits>& stream_;
	const std::ios_base::fmtflags flags_;
	const std::streamsize precision_, width_;
	const charT fill_;
	mutable bool is_last_;
};

template <class charT, class traits>
class saved_ostream
	: public saved_stream<charT, traits>
{
public:
	explicit saved_ostream(std::basic_ostream<charT, traits>& stream)
		: saved_stream<charT, traits>(stream), stream_(stream) { }
	
private:
	std::basic_ostream<charT, traits>& stream_;
	
	template <class T>
	friend saved_ostream operator < (const saved_ostream& saved, T& value)
	{
		saved.stream_ << value;
		return saved_ostream(saved);
	}
	template <class T>
	friend saved_ostream operator < (const saved_ostream& saved, const T& value)
	{
		saved.stream_ << value;
		return saved_ostream(saved);
	}
	friend saved_ostream operator < (const saved_ostream& saved,
		std::basic_ostream<charT, traits>& (*value)(std::basic_ostream<charT, traits>&))
	{
		saved.stream_ << value;
		return saved_ostream(saved);
	}
	friend saved_ostream operator < (const saved_ostream& saved,
		std::basic_ios<charT, traits>& (*value)(std::basic_ios<charT, traits>&))
	{
		saved.stream_ << value;
		return saved_ostream(saved);
	}
};

template <class charT, class traits, class T>
saved_ostream<charT, traits> operator < (std::basic_ostream<charT, traits>& stream, T& value)
{
	return saved_ostream<charT, traits>(stream) < value;
}
template <class charT, class traits, class T>
saved_ostream<charT, traits> operator < (std::basic_ostream<charT, traits>& stream, const T& value)
{
	return saved_ostream<charT, traits>(stream) < value;
}
template <class charT, class traits>
saved_ostream<charT, traits> operator < (std::basic_ostream<charT, traits>& stream,
	std::basic_ostream<charT, traits>& (*value)(std::basic_ostream<charT, traits>&))
{
	return saved_ostream<charT, traits>(stream) < value;
}
template <class charT, class traits>
saved_ostream<charT, traits> operator < (std::basic_ostream<charT, traits>& stream,
	std::basic_ios<charT, traits>& (*value)(std::basic_ios<charT, traits>&))
{
	return saved_ostream<charT, traits>(stream) < value;
}

template <class charT, class traits>
class saved_istream
	: public saved_stream<charT, traits>
{
public:
	explicit saved_istream(std::basic_istream<charT, traits>& stream)
		: saved_stream<charT, traits>(stream), stream_(stream) { }
	
private:
	std::basic_istream<charT, traits>& stream_;
	
	template <class T>
	friend saved_istream operator > (const saved_istream& saved, T& value)
	{
		saved.stream_ >> value;
		return saved_istream(saved);
	}
	template <class T>
	friend saved_istream operator > (const saved_istream& saved, const T& value)
	{
		saved.stream_ >> value;
		return saved_istream(saved);
	}
	friend saved_istream operator > (const saved_istream& saved,
		std::basic_istream<charT, traits>& (*value)(std::basic_istream<charT, traits>&))
	{
		saved.stream_ >> value;
		return saved_istream(saved);
	}
	friend saved_istream operator > (const saved_istream& saved,
		std::basic_ios<charT, traits>& (*value)(std::basic_ios<charT, traits>&))
	{
		saved.stream_ >> value;
		return saved_istream(saved);
	}
};

template <class charT, class traits, class T>
saved_istream<charT, traits> operator > (std::basic_istream<charT, traits>& stream, T& value)
{
	return saved_istream<charT, traits>(stream) > value;
}
template <class charT, class traits, class T>
saved_istream<charT, traits> operator > (std::basic_istream<charT, traits>& stream, const T& value)
{
	return saved_istream<charT, traits>(stream) > value;
}
template <class charT, class traits>
saved_istream<charT, traits> operator > (std::basic_istream<charT, traits>& stream,
	std::basic_istream<charT, traits>& (*value)(std::basic_istream<charT, traits>&))
{
	return saved_istream<charT, traits>(stream) > value;
}
template <class charT, class traits>
saved_istream<charT, traits> operator > (std::basic_istream<charT, traits>& stream,
	std::basic_ios<charT, traits>& (*value)(std::basic_ios<charT, traits>&))
{
	return saved_istream<charT, traits>(stream) > value;
}

このコードは一時オブジェクトを大量に生成するが、最適化が効けば通常は省略されるので特に問題とはならない。
もちろんフラグ以外の状態も復元される。自作のマニピュレータも自由に使える。

#include <iomanip>
using namespace std;

/* ... */

cout < setprecision(2) < hex < setw(15)
	< scientific < setfill('0') < 1234.5678 < endl;

cout << 1234.5678 < endl;
0000001.23e+003
1234.57

私としてはなかなか使いやすいと思うのだが、演算子が紛らわしいかもしれない。operator <とoperator >の部分を置き換えれば簡単に改造することはできる。ぜひお試しあれ。

このブログのフォント

普段はFirefoxしか使っていないので気づかなかったのだが、IE7だとこのブログはかなりひどいフォントで表示されていたらしい。
VistaのIE7でsans-serifがGulimになる - きよくらの備忘録
sans-serifだけを指定していると起きるバグ(?)らしい。ブログ用スタイル自体はマカーが作ったものらしく、いかにもMacっぽいフォントしか指定されていなかった。とりあえずWindowsのフォントを追加してみたので、若干マシになったかもしれない。

配列引数の要素数を調べる

配列を返す関数 - meryngii.neta
以前に配列を返す関数は参照を使えば作れることを紹介したのだが、同じように関数の配列引数の要素数も調べることができるらしい。

#include <iostream>

void f(int a[])
{
	std::cout << (sizeof(a) / sizeof(int));
}

int main()
{
	int x[10];
	f(x);
}

これは期待した動作をしてくれない。変数宣言でのint ar[]はint* arと同じ意味だから、fは常にintへのポインタの大きさを表示してしまう。

template <std::size_t Size>
void f(int a[Size])
{
	std::cout << Size;
}

今度はテンプレートパラメータで大きさを取得しようとしているのだが、これもうまくいかない。どうやら引数の部分にあるサイズの指定は完全に無視されて先ほどと同じようにポインタ引数と解釈されるらしい。*1しかもテンプレートパラメータが推測できないので、明示的に指定しないとコンパイルエラーとなってしまう。

template <std::size_t Size>
void f(int (&a)[Size])
{
	std::cout << Size;
}

結局、解決策は前回の戻り値の話と同じように参照を使うということになる。
もしC言語に最初から参照があったら、int ar[Size]はint (&ar)[Size]と解釈されるようになっていたかもしれない。C言語は参照という仕組み無しで配列の引き渡しをポインタで行えるように、配列とポインタのインターフェイスを統一してある。C++には参照があるのでもっとすんなり解決できそうだが、どうしても一部にC言語の奇妙な仕様が残ってしまった。
前回の結論と組み合わせると、こんな風になる。

#include <iostream>

int (&GetBuffer())[10]
{
	static int buffer[10] = {0};
	return buffer;
}

template <class T, std::size_t Size>
void ShowArray(T (&ar)[Size])
{
	for (std::size_t i = 0; i < Size; i++)
		std::cout << ar[i] << std::endl;
}

int main()
{
	ShowArray(GetBuffer());
}

ストリーム関連の命名はあまりにひどい

C++の教科書で必ず最初に現れるcoutとか<<とかいう謎の物体。C++の入出力ライブラリはオーバーロードを駆使することで型安全を堅持している。これはStroustrup氏の自信作で、その内部はテンプレートによって汎用性の高い設計になっている。
しかし、ストリームに関するクラス・関数・変数の命名規則はかなり混沌としている。これは同じ標準ライブラリである後続のSTLなどと比べてみても、明らかにひどい。とてもお手本にすべきものではない。

  • 全て小文字を使う。
  • 単語間はアンダーバーなどを入れずにそのままつなげる。
  • たまに不規則に単語を1文字〜数文字に縮める。

例えばマニピュレータ。

resetiosflags
setiosflags
setbase
setfill
setprecision
setw

私が思うにこの理屈でいくとsetwはsetwidthとなるべきだろう。幅の指定はよく使われるから短くしようという配慮なのかもしれない。
ストリームバッファの関数名はさらにとんでもないことになっている。

eback()
gptr()
egptr()
pbase()
pptr()
epptr()

順に入力バッファの先頭・現在・終端、出力バッファの先頭・現在・終端のポインタを返す。これらはもう名前から動作を予測しろと言われても不可能に近い。
C++のストリームに関しては、C言語のprintfと比べられることが避けられなかったという特有の事情が存在する。printfは書式文字列のおかげでタイプ数がとても少なく済んでいる。C++でも負けじと簡略化しようとしてこういうことになったのかもしれない。
「名前なんてどうでもいい」と思う人もいるのだろうが、ソースの初見で最も頼りになるのは間違いなく名前だ。ハンガリアン記法を強制したりなどといった過剰な命名規則の意識は不必要だが、1分くらい立ち止まって名前を考える努力は必要だと思う。
命名規則はいろいろあるから、自分の環境に合わせて好きなものを使えばいい。私が考える最低限の命名規則は次の二つだ。

  • アンダーバーでもキャメルケースでも何でもいいから単語を区切る。
  • 一文字の名前は使わない。*1

混沌としたソースを見るたびにこのくらいには何とかしてほしいなあと思う。

*1:i, jなどの使いかたがはっきりしたものは使ってもいいと思う。

volatileとコピー・代入

C++でvolatile修飾された構造体はものすごく扱いにくい。

typedef struct { int x; } X;

int main()
{
	X n_1;
	volatile X v_1;
	
	/* コピー(代入ではない)*/
	X n_2 = n_1; /* OK */
	X n_3 = v_1; /* エラー */
	volatile X v_2 = n_1; /* OK */
	volatile X v_3 = v_1; /* エラー */
	
	/* 代入 */
	n_2 = n_1; /* OK */
	n_3 = v_1; /* エラー */
	v_2 = n_1; /* エラー */
	v_3 = v_1; /* エラー */
	
	return 0;
}

このコードはC++では大量のエラーが出るが、C言語ではコンパイルが通る。おそらくCとC++の非互換性の一つではないかと思われる。
原因はC++のデフォルトコピーコンストラクタ・代入演算子の定義にある。

X::X(const X&)
X& X::operator=(const X&)

これらの関数の引数になったりthisポインタになったりするときに、volatileがついていると暗黙の型変換ができないのでエラーとなる。
これはC言語の型変換が甘いことや、C++の型チェックが厳しすぎることが原因ではない。C++のデフォルトの関数が足りないことが原因である。
一番望ましい対策は標準でこれらを定義することだと思う。

X::X(const volatile X&)
volatile X& X::operator=(const volatile X&) volatile
X& X::operator=(const volatile X&)
volatile X& X::operator=(const X&) volatile

パフォーマンスを考慮しないなら下の二つは必要ない。
volatileはあまり使われていないのだろう。*1この件に関して見つかったのは2chのレスぐらいだった。*2しかしC++C言語との互換性を重視しているはずなので、ぜひとも検討してほしい。
ちなみに、volatileが絡む例で一つだけコンパイルが通っている行がある。

volatile X v_2 = n_1; /* OK */

v_2はvolatileオブジェクトだが、構築中であるのでエラーにはならない。コンストラクタでthisポインタが修飾されることはないので、例えば次のようなコンストラクタは定義できない。

X::X() volatile { } // エラー

さて、ユーザレベルで対応するにはキャストするのが手っ取り早い。

template <class T>
inline void Assign(volatile T& dest, const volatile T& src)
{
	const_cast<T&>(dest) = const_cast<const T&>(src);
}

結果は保証されないだろうが、現実的にはあまり問題は無いと思う。

*1:組み込みでは大量にvolatileをばらまくことがよくある。

*2:http://pc11.2ch.net/test/read.cgi/tech/1234420483/182 海外のコミュニティにはありそうな気がする。

テンプレート名と継承元

C++ではテンプレート名が型名になるという仕様があるのだが、
クラステンプレート名は型名 - meryngii.neta
これはなぜか継承元の指定では使えないらしい。

template <class T>
struct A { };

template <class T>
struct B
	//: A<B> // 1, NG
	: A< B<T> > // 2, OK
{
	typedef A< B<T> >	base1; // 3, OK
	typedef A<B>	base2; // 4, OK
};

VC8では1の部分のコードは通らないが、VC7までは使えたようだ。
http://www.ailight.jp/blog/sha256/archive/2005/08/04/9214.aspx
GCCはVC8と同じ挙動を示すので、言語仕様としてはこれが正しいのだろう。ただし理由は分からない。
この手の記法は自己言及的なテンプレートを使うときによく出てくる。
http://d.hatena.ne.jp/Cryolite/20040605

エスケープと一行コメント

// 使用可能
int x;

場合によってxは使用できないかもしれない。
Shift-JISにおいて「能」は2バイト目が\(0x5C)なので「だめ文字」と呼ばれる。これらの文字は配慮の足りないプログラムからエスケープと見なされて文字化けなどの問題を起こす。
また一行コメントは行の終わりが\だと次の行もコメントになる。#defineでダラダラとマクロを書くときと同じだ。物理行から論理行に変換される過程で一行にまとめられるらしい。
http://cppemb.blog17.fc2.com/blog-entry-35.html
結果、Shift-JISで保存すると前述のxはコメントになってしまう。
この問題はGCC*1を使っている時に気づいた。ちなみにVC++ではこの問題は発生しなかった。いわゆるバッドノウハウなのだと思う。その場しのぎの対策としては後ろにスペースを入れるだけだが、いつか困ったバグになるかもしれない。

*1:--input-charsetで解決できるらしい。