2005-08-06
名前空間は、C++を「より良いC(better C)」として使う場合にも、本格的な使い方をする場合にも、非常に強力な機能を提供してくれます。しかも、速度面、サイズ面の両方において、まったくオーバーヘッドを伴いません。
ところで、C++には2種類の名前空間があります。一つは、namespaceというキーワードを用いて明示的に定義される名前空間であり、もう一つは構文上の概念としての名前空間です。C++で名前空間といえば、通常前者を指しますが、参考までに、後者についても簡単に説明しておきます。
後者の名前空間はCにも存在していました。それは、「ラベル名」、「メンバ名」、「タグ名」、およびその他の識別子に対して、同名の識別子を用いても区別できるようにするためのものです。これは、前者のnamespaceを用いて定義する名前空間とまったく別のものです。
さて、それでは本題であるキーワードnamespaceを用いた名前空間に付いてお話します。
名前空間は
Embedded C++(EC++)では削除されていますが、それは、EC++の仕様が検討されていた時点では、名前空間の詳細な仕様が流動的であったためです。名前空間は単純そうでいて、実引数依存の名前検索など、やや難解な仕様を含んでいるのです。
名前空間を使うには、以下のように宣言を囲むことで行います。また、名前空間を入れ子にすることもできます。
namespace 識別子 {
宣言の並び
}
特定の名前空間内の識別子にアクセスするには、名前空間名::識別子のようにします。特にグローバルな名前空間(キーワードnamespaceで囲まれていない名前空間)を指定する場合は、::識別子のようにします。
名前のない名前空間(無名名前空間)を定義することもできます。無名名前空間は、内部で翻訳単位ごとにユニークな名前が付けられます。これにより、キーワードstaticを使わなくても、同じ翻訳単位からのみアクセスできる識別子を宣言することができます。
名前空間の機能のとしてこれぐらいですが、先に少し触れた「実引数依存の名前検索」のような名前検索に関わるやや難解な仕様があります。他にも、名前の隠蔽に関わる仕様もあります。名前の隠蔽の方が簡単ですので、先にこちらからお話したいと思います。
名前空間の中で識別子を宣言すると、外部にあった同名の識別子が隠蔽されてしまいます。まずは下記のコードを見てください。
void foo(int);
namespace bar {
extern const char foo[];
inline void hoge(int x)
{
foo(x); // エラー
}
}
上のコードでは、グローバル名前空間で宣言したfoo関数をbar名前空間内のインライン関数hogeで呼び出そうとしていますが、コンパイルエラーが発生してしまうことを意味しています。これは、bar名前空間内で宣言した配列fooによって、グローバル名前空間のfooが隠蔽されたからです。
こうした名前の隠蔽は、関数内のブロック内で同名の宣言を行うとブロック外の識別子にアクセスできなくなるのと同じことです。関数の場合は、同名の識別子の宣言はできれば避けるべきです。しかし、名前空間の場合、同名の識別子が宣言して初めて存在意義があるのでそうはいきません。
こうした名前の隠蔽は、名前空間だけでなく、クラスの定義でも同様のことが起こるわけですが、C++は同名意義の識別子が発生することは不可避であることを、必ず心に留めておく必要があるわけです。
次に「実引数依存の名前検索」ですが、これについても最初にコードを見ていただきましょう。
void foo(int);
namespace A {
enum bar { hoge, piyo };
void foo(bar);
}
void test()
{
A::bar x = A::hoge;
foo(x);
}
上記のコードで、test関数内で呼び出しているfoo関数は、::fooでしょうか?それともA::fooでしょうか?もし、コンパイラが手元にあるようなら、是非試してみていただきたいと思います。
一見すると、グローバル名前空間の::fooが呼ばれるように思いますが、実際にはA::fooが呼ばれます。これは、呼び出すべき関数を検索する際には、実引数に指定したxの型A::barが属している名前空間も検索対象になるからです。だからこそ「実引数依存」なのです。
この仕様は意外に便利ですし、多重定義された演算子のように、こうなっていないとまずいものさえあります。しかし、この機能によって、名前空間の内外は完全に隔絶されたものではなく、微妙に干渉し合う結果になってしまっています。例えば、
namespace A {
enum bar { hoge, piyo };
}
void func(A::bar);
void test()
{
A::bar x = A::hoge;
func(x);
}
のようなコードでは、関数testから呼び出される関数funcは、当然、グローバル名前空間にある::funcです。ところが、名前空間Aの中で、引数にA::barをとる関数funcを後から追加してしまった場合は悲惨な結果になります。
つまり、本来であれば、関数testから呼び出されるべきfuncは::funcであるべきなのですが、名前空間Aの中にもA::funcが見つかると、どちらを呼び出すべきかが曖昧になってしまいます。このように、名前空間の中に後から宣言を追加すると、クライアントコードを破壊する恐れがあるわけです。
以上のように、名前空間にはやや難しい側面もありますが、この機能を使うことで、だらだらと長い接頭辞を付ける必要はなくなります。注意点を確実に押さえた上で、有効活用していただければと思います。
Comment