移植性の高いプログラムの書き方
「stdの中か外か」編

しのはらのC++実験室トップページ
「移植性の高いプログラムの書き方」トップページ

I/Oストリームライブラリとstd名前空間

 「using指令を使うときの注意点」で述べたように、I/Oストリームなどの標準ライブラリのクラスは、std名前空間の中で宣言されます。しかし、古いC++の仕様に基づくコンパイラや過渡期に作られたコンパイラでは、I/Oストリームライブラリがグローバル名前空間で宣言されている場合があります

 一般的に言って、消極的な理由によって古い仕様に従ったプログラムよりも、新しいスタイルを積極的に使ったプログラムの方が、僕は好きです。新しいものが好きといっているのではなく、C++の新しいスタイルがすごく合理的だから、そういうのは積極的に取り入れてプログラミングすべきだと、言いたいのです。

 さて、最新のスタイルではistreamやcoutなどのクラスやオブジェクトはstd名前空間で定義されるので、修飾子std::を付けて、std::istreamstd::coutなどのように使用することになります。しかし古いコンパイラでは事情が異なるためそのようなコードはエラーになってしまいます。僕は新しいスタイルに従うことの重要性を説いているのですから、ここで当然、新しいスタイルを古いコンパイラに分からせる方法を紹介することになります。こういう積極的解決方法に気付く人は少ないようですが、以外と単純な仕掛けで実現します。

新しいスタイルを古いコンパイラに分からせる

 まずここで、僕が考えたシンプルな方法を紹介します。I/Oストリームライブラリがグローバル名前空間に配置されるような古いコンパイラの場合には、予めマクロM__NO_STD_IOSTREAMが定義されているとします。そして、I/Oストリームライブラリを使うすべてのコードで、次のようなコードをインクルードします。

#ifndef M__NO_STD_IOSTREAM
	// 方法@ 新しいコンパイラの場合
	#include <iostream>
#else
	// 方法A 比較的古いコンパイラの場合
	#include <iostream.h>
	namespace std{
		using ::istream;
		using ::ostream;
		using ::cout;
		using ::cerr;
		using ::cin;
		using ::endl;
	}
#endif

 これは、新しいコンパイラの場合にはiostreamヘッダをインクルードするだけです。古いコンパイラの場合、古い形式のインクルード指令を使用して、さらに主要なクラスやオブジェクトについて自前のstd名前空間の中でusing宣言しています。ここがポイントです。上のようなコードを使用することで、いつでもstd::coutなどの新しい記法が使えるようになります。

結構うまくいくが、注意点がある

 上に示したコードには、気を付けるべき点がいくつもあります。ですがちゃんと気を付けて使うならばとてもうまく働く方法であることは保証できると思います。

 注意点の一つは、使いたいすべてのクラスやオブジェクトについて、いちいちusing宣言を追加しなければならないことです。これは当然のことですが、面倒なことです。

 またこのテクニックを使う場合は、I/Oストリームライブラリを使うすべての場所で、iostreamヘッダやiostream.hをインクルードする代わりに上記のコードをインクルードするようにする必要があります。

 そして重要なことは、使用するコンパイラがどちらのスタイルのI/Oストリームライブラリを提供しているかを確認し、それにあわせてマクロM__NO_STD_IOSTREAMをdefineするかどうかを切り換えることです。これは、個々のコンパイラとそのバージョンごとに事情が異なりますから、個別に調べて#ifdefなどで対応するほかありません。簡単な確認方法としては、まず方法@(iostream)で試して、下に示すようなサンプルコードが通らない場合は、方法A(iostream.h)に切り換えればよいでしょう。

int main()
{
	std::cout << "Hello world." << std::endl;
	return 0;
}

じつは問題点もある

 ここまで説明しておいてじつは問題点がありますというのは、話の順序としてよくないかもしれませんが、ここまででこのテクニックの原理がとても単純であることが、分かって頂けたと思います。

 問題点というのは、ある種の過渡的なコンパイラで発生します。それは、std名前空間に納められた新しい形式のI/Oストリームライブラリが存在するが、その機能が不完全で使い物にならない場合などです。その場合、古いiostream.h形式のライブラリを使用するために上に示した方法Aを試しても、うまくいかない場合があります。コンパイラがstd名前空間における識別子重複エラーを報告するのです。

問題点を解決する一つの方法

 そのような場合にも使用可能な次善のコードは次に示すものです。ここで、マクロM__DONOT_USE_STD_IOSTREAMは新しいI/Oストリームライブラリを使用するかどうかを指定します。

#if !defined(M__DONOT_USE_STD_IOSTREAM) && !defined(M__NO_STD_IOSTREAM)
	// 方法A 新しいI/Oストリームを使う場合
	#include <iostream>
	namespace mystd{
		namespace std = ::std;
	}
#else
	// 方法B 古いI/Oストリームを使う場合
	#include <iostream.h>
	namespace mystd{
		namespace std{
			using ::istream;
			using ::ostream;
			using ::cout;
			using ::cerr;
			using ::cin;
			using ::endl;
		}
	}
#endif

 このように独自の名前空間mystdを用意してその中にローカルなstd名前空間を定義します。こうすると、たとえグローバルなstd名前空間にI/Oストリームライブラリが存在しても衝突を避けられるわけです。使い方は次のようになります。

namespace my_program{
	using namespace mystd;
	void f(){
		std::cout << "Hello world." << std::endl;
	}
}

 ポイントはusing namespace mystd;を記述することにあります。こうすることで、グローバルなstd名前空間に優先して、自前のローカルなstd名前空間を使用できるようになります。これで、新しい形式のI/Oストリームライブラリが存在する場合でも、自分の都合にあわせて実際に使用するライブラリを選択することができるようになるのです。

 ここでの注意点は、このテクニックは必ず何らかの名前空間(上の例ではnamespace my_program)の中でしか使用できないことです。グローバル名前空間においてusing namespace mystd;を宣言してはいけません。その場合は次に示すように曖昧エラーを生む可能性があるからです。

// 良くない使用例
using namespace mystd;	// グローバルなusing指令
void f(){
	std::cout << "Hello world." << std::endl;
		// ↑[エラー] std::cout と mystd::std::cout が曖昧です。
}

 グローバル関数の中でのこの問題を解決するには次のようにします。

void f(){
	using namespace mystd;	// 関数ローカルなusing指令
	std::cout << "Hello world." << std::endl;
}

まとめ

 結局、こういうコンパイラの違いからくる問題に対処するには、個々のケースにあわせてがんばって問題を解決するしかないとも言えますが、コンパイラの特性がいくつかのタイプに分類できるならば、今回のようにそれらのどれが来ても働くようなコードを書くことで互換性を実現することができると思います。僕は、そのような工夫の積み重ねは、現実的な移植性維持作業のために効果的だと考えています。

 なによりも大事なことは、将来標準に限りなく準拠したコンパイラが現れたときに、確実に動く新しいスタイルのコードを書くようにいつも心がけることです。そのためには現在のコンパイラでそのようなコードが通るようにする必要があり、その時に上に示したようなテクニックが役に立つものと考えています。