予約名

Visual Studio .NET 2003

Robert Schmidt
Microsoft Corporation

2000 年 4 月 20 日

名前の衝突というのは、プログラミングをしていれば必ずぶつかる現実です。自分が使おうとした名前が、ほかのところ、つまり同じプロジェクトの仲間、windows.h、標準ライブラリなどによって、まったく異なる目的のためにすでに使われていたというのは、プログラマならだれでもいつか必ず経験することです。その場合、名前を再定義するか、そのまま借用することになります。この問題は、コンパイル時のエラーですぐわかる場合もあれば、原因不明のビルド エラーや厄介なプログラム バグに至るまで、ありとあらゆる結果となって現れます。.

名前を踏み散らすように使ってしまうプログラマの性癖を考慮して、標準規格は 1 つの取り決めを提供しています。標準規格では、言語、標準ライブラリ、およびトランスレータの実装開発者に対して、一定の名前セットが予約されています。その代わり、それ以外の名前はすべて私たちプログラマのために予約されています。プログラマは暗黙のうちに、標準規格による予約名を絶対に使わないように約束させられています。この約束を破れば、プログラムは規格に準拠しません。同様に、コンパイラや標準ライブラリの開発者も標準規格による予約名だけを使うように約束させられています。予約名以外の名前を使えば、その実装系は規格に準拠しません。

以上は、理論上の話です。実際には、名前に関する取り決めの境界線をどこに引くかについて、大部分のプログラマの考え方がどう見ても混乱しています。プログラマがこの境界線を越えたときの違反は、多くの場合偶然発生します。それより許し難いのは、標準規格の徒であるべきコンパイラの実装開発者による違反です。彼らが間違いを犯すときの原因は、偶発的なものより詳しい情報に基づいて意識的に選択したものである可能性が高いからです。

キーワード

標準規格で予約されているキーワードはどっさりあります。C90 規格で 32 個、C99 規格で 37 個、C++ 規格で 63 個あります。キーワードは識別子に似ていますが、言語の構文規則の中で特別な意味を持っています。プログラマもコンパイラ ベンダも、規格に準拠したプログラムの中でキーワードを識別子として使うことはできません。

次のようにキーワードをあえて識別子として使うと、

int char; /* error */

通常コンパイラが違反を知らせます。唯一の例外は、次のようにキーワードをマクロとして定義した場合です。

#define char abc

int char; /* OK */

コンパイラの翻訳手順は規格によって決められています。コンパイラはキーワードの文字列を解析する前に、プリプロセッサ ディレクティブ(#define)を実行してマクロ(char)を展開します。したがって、#define にキーワードを定義した違反をコンパイラが検出しようとする頃には、その証拠が消えているわけです。

コンパイラとしては、このエラーを間接的に知らせるのが精一杯です。

#define char abc

char *p;

このような例を与えると、Visual C++ は仕方なく次のような メッセージを吐き出します。

syntax error : missing ';' before '*'
'abc' : missing storage-class or type specifiers
'p' : missing storage-class or type specifiers

このメッセージのどこを見ても本当の問題のありかはわかりません。

単純な教訓:キーワードを #define などでねつ造しないこと。

残念ながら、この教訓は Microsoft のやり方と合い入れません。Visual C++ プロジェクト ウィザードで MFC アプリケーションを新規作成すると、出力される .cpp ファイルの冒頭に標準で次のような部分が含まれています。

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

2 行目は、キーワード new に対して DEBUG_NEW というエイリアスを定義しています。このエイリアス自体は、Microsoft 独自のヘッダー ファイル afx.h で定義されているマクロです。DEBUG_NEW THIS_FILE __LINE__ を参照しているので、これによってニセの new が含まれているソースのコンテキストをキャプチャしてトレースやデバックができるようになります。

公正を期すなら、Microsoft は new を「オーバーロード」することで、プログラマが長く親しんできた表記方法をそのまま使えるようにしただけだと理解すべきでしょう。また、MFC プログラムはすでにいくつもの点で規格違反を犯していて、#define new はそのほんの一例にすぎません。それでもやはり、このやり方はお勧めできません。将来、事情を知らずにこのコードを読んだり管理したりする人を混乱させるからです。代わりに、NEW とか traced_new といった標準規格の予約語以外の名前を定義して、通常はニセの new を使う場所にその名前を使うことをお勧めします。

認識されないキーワード

C++ 規格で定義されている 63 個のキーワードのうち、Visual C++ で正しく認識されるのは asmexport、および wchar_t 以外のキーワードです。その証拠に、IDE のエディタ内でこの 3 つのキーワードだけは該当する構文表示色で表示されません。

コンパイラは、asm を標準規格外の擬似キーワードのようなものと見なしているようです。Visual C++ のマニュアルでは、asm は「ほかの C++ 実装系との互換性のために予約されているが、実装はされない」と説明されています。

それに対して、export wchar_t はコンパイラからまったく認識されません。したがって、標準規格に違反してこの 2 つを普通の識別子として宣言することができます。

void export(int wchar_t);

もっと悪いことに、Microsoft 自体の標準ライブラリのヘッダーが wchar_t を識別子として定義しています。

typedef unsigned short wchar_t;

その結果、

void f(wchar_t)
   {
   }

void f(unsigned short)
   {
   }

というコードはコンパイルされません。コンパイラは、

f(unsigned short)

という定義が重複していることを指摘します。これをあら捜しだと思われると困るので、Microsoft が昔から大事にしている BSTR が実は whcar_t * typedef だということも考えてください。このため、

void f(BSTR)
   {
   }

void f(unsigned short *)
   {
   }

というコードは、たとえこの 2 つのパラメータ タイプに論理的な関係がなくても書くことはできないのです。

C90 規格では wchar_t typedef なので、Microsoft は下位互換性を維持するために、自社の C++ コンパイラではこれを typedef のままにしたのではないかとも思われます。それよりもわからないのは、export がなぜまったくサポートされないかです。Visual C++ には、このキーワードの意味は無視するとしても、少なくともその構文だけは認識してもらいたいものです。先例もあります。Microsoft の古い DOS ベースの C コンパイラは(たしかバージョン 5 だったと思いますが)、キーワード volatile の意味を無視して構文だけを受け入れていました。

Microsoft を擁護する立場で言えば、export はテンプレートの初期化に関していわゆる分離モデルをサポートするキーワードですが、分離モデルをきちんとサポートするトランスレータはないと言ってよく、EDG のトランスレータも例外ではありません(ただし EDG は export キーワードを構文としては受け入れます)。

その他の予約名

C++ 規格では、通常記号文字で表す演算子の「代替表現」を 11 個追加しています。

演算子   代替表現
--------   -----------
   &&         and
   &=         and_eq
   &          bitand
   |          bitor
   ~          compl
   !          not
   !=         not_eq
   ||         or
   |=         or_eq
   ^          xor
   ^=         xor_eq

非英語圏のコンピュータ システムでは、

& | ~ ! ^

といった文字を使えないことがあります。代替表現は、そういうシステムでは不可能だったソース構文の直接入力による表現を可能にするものです。

x = a || b;
x = a or b; // equivalent

Visual C++ ではもともとこのような表現をまったくサポートしていないので、次のようなことも可能です。

int or;
void not(char *compl);

wchar_t と同様、理由は下位互換性にあると見ています。C90 規格では、これらの表現を予約語として扱わず、その代わり次のように標準ライブラリ ヘッダーをインクルードすることを要求しています。

#include <iso646.h>

int x = not 1; // OK
int or;        // error

Microsoft の C++ コンパイラでも、<iso646.h> <ciso646> のどちらかが必要になります。規格に準拠した C++ 実装系ではこれらのヘッダーは空になるので、後で面倒なことになることもありません。良心的な言い方をすればヘッダーを入れる必要がある、批判的な言い方をすればこのヘッダーによる影響は少ない、ということです。

予約名の形式

Visual C++ の <iso646.h> の中を覗いてみると、次の行が見つかります。

#define _ISO646

_ISO646 は、標準規格で要求されている名前ではなく、Microsoft が自社の標準ライブラリ ヘッダーの実装のために使うことを選んだ名前です。ほかのコンパイラ ベンダが提供している <iso646.h> を調べると、おそらくほかの実装名が使われていることがわかるでしょう。たとえば、Metrowerks の <iso646.h> では、同じ目的に __iso646_h が使われています。

当然、プログラマが定義した名前がたまたま _ISO646 だったとすれば、Microsoft の定義とぶつかってしまいます。このような衝突を回避するため、標準規格は Microsoft などのベンダが自社のコンパイラやライブラリの実装に使用できる名前の形式を規定しています。

この規定は 2 つの言語で微妙に異なっています。C90 および C99 規格では、次の名前が実装系のために予約されています。

  • グローバル スコープを持ち、_ で始まる名前

  • _ で始まり、その次が大文字の名前

  • __ で始まる名前

C++ 規格では、次の名前が実装系のために予約されています。

  • グローバル スコープを持ち、_ で始まる名前

  • _ で始まり、その次が大文字の名前

  • __ を含む名前

(C++ の規定の方が予約名の範囲が大きく、2 連のアンダースコア文字が名前の先頭だけでなくどこにあっても予約名になる。)

コードの規格への準拠と移植性を最大限に保つため、自分のプログラムでこのような名前を定義することは避けてください。この規定を覚えやすくするため、2 連のアンダースコア文字と先頭のアンダースコア文字は言語やスコープに関係なく常に避けるのがよいでしょう。

私の経験から言うと、プログラマは次のようにインクルード ファイルのガードを作成するときに、よくこの規定を破りがちです。

#if !defined _DOOFUS_H_
    #define  _DOOFUS_H_

/* ... */

#endif

プログラマは、こういう名前の前後や中にアンダースコア文字を好んで入れるようです。この性癖の歴史的経緯としては、次の 2 つが考えられます。

  • インクルード ファイルの名前を真似る

  • 名前を「汚く」することで、実装の詳細であるを強調し、ヘッダーの外から参照すべきでないことを示す

Microsoft のプロジェクト ウィザードもこの規定に違反しています。上記で引用した、ウィザードが生成した MFC プロジェクトには、どんな場合にも必須のヘッダーである stdafx.h が含まれています。このヘッダーには次のような形のガード マクロが定義されています。

AFX_STDAFX_H__E223DB32_0FCD_11D4_8557_0000C5581074__INCLUDED_

2 連のアンダースコアに注目してください。このヘッダーは実質的には Visual C++ が作成するとはいえ、このコードはコンパイラの実装や標準ライブラリとは関係ありません。したがって、この定義はユーザー定義名に関する標準規格の規定に違反しています。

この名前自体は GUID を埋め込んで作られているので、ほかでまったく同じ名前が定義されて衝突する可能性は少ないと言えます。例外処理シリーズの第 2 部 では、より確実な例を取り上げました。ATL のプロジェクト ウィザードで作成したユーザー コードの中に定義される 2 つのグローバル関数 _Handler _ServiceMain です。ATL や MFC のプログラムはすでに規格に準拠していない、つまり標準規格の名前に関する規定の枠外にあるのだから、これらの名前は規定違反に当たらない、という主張もあるでしょう。しかし、そういう主張は、コードの移植性やコード管理について直面すべき現実に目をつぶっているだけなのです。

Microsoft 以外の実装系でそれぞれの目的に _Handler _ServiceMain が使われることを防ぐ手立てはありません。実のところ、Microsoft 自体の標準ライブラリ ヘッダーの将来バージョンでこれらの名前が使われない保証すらないのです。これらの実装系が標準規格の規定に従っているなら、たとえ規定に従う必要のない場合でも、無用の危険を避けて規定に従っておく方がプログラマの身のためです。言いかえれば、標準規格の規定は、たとえそれに従う義務がない場合でも、アーキテクチャに関する便利な手引きとして利用できるのです。

名前空間 std

ユーザー コードと標準ライブラリ部品の間でこれ以上の衝突を防ぐため、C++ 標準化委員会はほぼすべての標準ライブラリ定義を名前空間 std に移動させました。名前空間はオープンなので、プログラマが自分の宣言を std に追加することも理論的には可能ですが、標準規格ではこのような追加をはっきりと禁止しています(定義済みのテンプレートを特殊化したテンプレートの追加は可能です。元の一般化テンプレートを標準規格の規定に従って特化することが条件です)。

標準規格では、どの宣言に std を付けるべきかが規定されていますが、ここまで来ると見当がつくように、Visual C++ では標準規格の規定が完全には守られていません。Microsoft の標準ライブラリ ヘッダーの中には、標準規格が std の中に入れるように要求しているエンティティでさえ、グローバル スコープで宣言しているヘッダーがあります。以下は、その例です。

  • <exception> の宣言の一部、terminate unexpected など

  • <new> の宣言 new_handler set_new_handler

  • <typeinfo> class type_info 用の宣言

  • <cstdio> などの)ヘッダーに含まれる、標準 C ライブラリの機能をラップするための宣言(この問題については例外処理シリーズの第 1 部 で取り上げた)

このような規格違反があるため、このままでは標準ライブラリの要素をすべて std:: で参照する方法をとることができません。代わりに、別の方法をとる必要があります。

代替手段 1:ディレクティブの使用

Visual C++ では、次のように標準ライブラリの名前の一部がグローバル スコープに置かれています。

#include <exception>
#include <string>

int main()
   {
   std::string s;
   terminate(); // should require 'std::terminate()'
   return 0;
   }

そこで、標準ライブラリの名前をすべてグローバル スコープに置いてやれば、うまくいきそうです。

#include <exception>
#include <string>

using namespace std;

int main()
   {
   /*std::*/string s;
   terminate(); // should require 'std::terminate()'
   return 0;
   }

このようにすれば、標準ライブラリの要素をすべて std の外で宣言されたものとして参照できます。その結果、ライブラリの要素がすべて同じスコープを持つように見えるため、Visual C++ の規格違反を隠すことができます。

この方法なら、結局翻訳単位ごとに 1 行追加するだけなので、実装が簡単に済むのが魅力ですが、名前空間 std の(標準ライブラリのあらゆる要素をユーザーのグローバル スコープから排除するという)目的がまったく損なわれてしまいます。インクルードする標準ライブラリ ヘッダーの数によっては、using ディレクティブによって大量の名前がグローバル スコープに詰め込まれるので、名前が衝突する可能性が増えることになります。

代替手段 2:宣言の使用

using ディレクティブは、std の名前をグローバル スコープに移動しました。グローバルの名前を 1 つ 1 つ std に移動することによって、その逆を行うこともできます。

#include <exception>
#include <string>

//using namespace std;

namespace std
   {
   using ::terminate;
   }

int main()
   {
   std::string s;
   std::terminate();
   return 0;
   }

この方法は痛烈な皮肉になっています。前述のように、標準規格はプログラマが名前空間 std に名前を追加することを禁じています。この場合、名前を標準規格で指定されている場所に追加しただけなのですが、結局のところ、高い次元で規格への準拠を実現するために、わざと規格に違反する処置を講じていることになります。

この using 宣言の対象は、::terminate の名前であってその関数書式ではないことに注意してください。この using 宣言は、::terminate という名前でオーバーロードされたすべての関数に対して、関数書式に関係なく作用します。したがって、using 宣言は、1 つの関数だけでなく、同じ名前で宣言される可能性のある関数ファミリーの宣言として考えるべきです。

また、名前空間 std にグローバル スコープを持つ名前の「クローン」を作ると、Visual C++ の実装系に混乱が生じる危険性もあります。Visual C++ の実装系は、std の中にそのような名前があることを想定していないためです(それを想定すべきかどうかは別として)。私はこのテクニックを何年にもわたって特に支障もなく使っていますが、事実上 Visual C++ の実装系を改造することになるので、どんな場合でもうまくいくとは保証できません。これらの名前が Visual C++ で正しく宣言されれば、必ず支障をきたすことになります。

代替手段 3:ヘッダーの置き換え

上記のように明示的に using 宣言を入れる代わりに、ヘッダー内の関連する宣言を閉じ込めてしまうこともできます。

//
// exception.h
//.
#if !defined exception_h_
    #define  exception_h_

#include <exception>

namespace std
   {
   using ::terminate;
   using ::unexpected;
   // ... and all other '<exception>' names that should be in 'std'
   }

#endif // !defined exception_h_

このカスタム ヘッダーを、標準ライブラリ ヘッダーの代わりにインクルードします。

//#include <exception>
#include "exception.h"
#include <string>

int main()
   {
   std::string s;
   std::terminate();
   return 0;
   }

こうすれば、コードの内容をほとんど変えることなく、実質的には規格に準拠した標準ライブラリ実装と同じ効果が得られます。

現在と将来のプラットフォーム間での移植性を考えるなら、すべての標準ライブラリ ヘッダーについて同じような代理ヘッダーを作り、「本物」のヘッダーの代わりにインクルードするとよいでしょう。こうすれば、ソース コードの大部分には手を触れずに済みます。規格に準拠しない部分はすべて代理ヘッダーの中に閉じ込められるからです。

Robert Schmidt は MSDN のテクニカル ライターです。このほかに彼が寄稿している雑誌に、C/C++ Users Journal があります。そこでは、彼は編集補助およびコラムニストとして活躍しています。これまでのキャリアで、ラジオ DJ、野生生物飼育係、天文学者、ビリヤード場管理者、探偵、新聞配達夫、そして大学講師を経験しています。

Deep C++用語集

表示: