標準モードは、C++ コンパイラのデフォルトの動作モードです。このため、標準モードを指示するオプションを指定する必要はありません。指定する場合は、以下のオプションを使用してください。
-compat=5
CC -O myfile.cc mylib.a -o myprog
C++ 標準では、新しいキーワードがいくつか追加されています。これらのキーワードを識別子として使用すると、多数の、ときとして意味不明のエラーメッセージが出力されます (キーワードがどこで識別子として使用されたかを判断することはかなり難しく、そうした場合は、エラーメッセージが役に立たないことががあります)。
次の表に示すように、新しいキーワードの大部分は、コンパイルオプションを使用して無効にできます。論理的に関連のあるオプションは、グループ単位で有効または無効にすることもできます。
表 3-1 標準モードで有効なキーワード
キーワード |
無効にするコンパイラオプション |
---|---|
bool、 true、 false |
-features=no%bool |
explicit |
-features=no%explicit |
export |
-features=no%export |
mutable |
-features=no%mutable |
namespace、using |
なし |
typename |
なし |
and、and_eq、bitand、compl、not、 not_eq、or、bitor、xor、xor_eq |
-features=no%altspell (下記の注を参照) |
特殊なトークンの代わりに使用できる文字列 (トークン代替文字列): ISO C 標準の追補には、特殊なトークンを生成するための新しいマクロを定義した C 標準のヘッダー <iso646.h> が導入されています。C++ 標準では、これらの文字列は予約語と定義されています (代替文字列が有効な場合、プログラムに <iso646.h> をインクルードしても何の働きもしません) 。これらのトークンの意味は、次の表に示すとおりです。
トークン |
代替文字列 |
---|---|
&& |
and |
&&= |
and_eq |
& |
bitand |
‾ |
compl |
! |
not |
!= |
not_eq |
|| |
or |
| |
bitor |
‾ |
xor |
‾= |
xor_eq |
C++ 標準にはテンプレートに関する新しい規則がいくつか導入されています。そのため、既存のコードが標準から外れたものになってしまう可能性があります。特に、新しいキーワード typename を使用しているコードがこれに該当します。5.0 コンパイラでは、それらの規則はまだ強制はされていませんが、キーワード自体は認識されます。4.2 コンパイラでは、不正なテンプレートコードが一部受け入れられることになり、4.2 コンパイラで動作していたテンプレートコードは、5.0 コンパイラでもおそらく動作します。将来的には新しい規則が適用されるため、開発スケジュールが許すかぎり、既存のコードは新しい C++ 規則に準拠させてください。
C++ 標準には、識別子が型名であるかどうかを判定するための新しい規則が導入されています。次の例で、それらの規則について説明します。
typedef int S; class B { ... typedef int U; ... } template< class T > class C : public B { S s; // OK T t; // OK U x; // 1. C++標準では無効 B::U y; // 2. C++標準では無効 T::V z; // 3. C++標準では無効 };
新しい言語規則では、テンプレート中の型名を解決するために、基底クラス名が自動的に検索されることはないと規定されています。また、キーワードの typename で宣言されていないかぎり、基底クラスやテンプレートパラメータクラスからとられた名前が型名になることはないとも規定されています。
上記の例の最初の無効な行 (1.) では、修飾クラス名とキーワードを使用せずに B から U を型として継承しようとしています。2 行目の無効な行 (2.) では、型は基底クラスの名前によって正しく修飾されてはいますが、typename 修飾子がありません。3 行目の無効な行 (3.) では、テンプレートパラメータからとられた型 V が使用されますが、キーワードの typename が省略されています。この型が基底クラスやテンプレートパラメータのメンバーに依存することはないため、s の定義は有効です。同様に、t の定義では、型の T (型である必要があるテンプレートパラメータ) がそのまま使用されるため、有効になります。
typedef int S; class B { ... typedef int U; ... } template< class T > class C : public B { S s; // OK T t; // OK typename B::U x; // OK typename B::U y; // OK typename T::V z; // OK };
コードを変更するときに問題になるのは、以前は typename がキーワードではなかったということです。既存のコードで typename を識別子として使用している場合は、まず識別子を別の名前に変更する必要があります。
新旧のコンパイラのどちらでもコードがコンパイルされるようにするには、プロジェクト全体で使用されるヘッダーファイルに次の例に示すような文を追加します。
#ifdef TYPENAME_NOT_RECOGNIZED #define typename #endif
これらの行を追加することにより、条件付きで typename が何ものにも置き換えられなくなります。typename を認識しない古いコンパイラ (Sun C++ 4.2 など) を使用する場合は、メークファイル中のコンパイラオプションに -DTYPENAME_NOT_RECOGNIZED を追加してください。
ARM と 4.2 コンパイラには、テンプレート定義を使ってテンプレートを明示的にインスタンス化する標準的な方法がありませんでした。C++ 標準と 5.0 コンパイラの標準モードには、テンプレート定義を使って明示的にインスタンス化する構文 (キーワード template の後に型を宣言する) が追加されています。たとえば、次のコードの最後の行では、デフォルトのテンプレート定義を使って、クラス MyClass を型 int でインスタンス化しています。
template<class T> class MyClass { ... }; template class MyClass<int>; // 明示的なインスタンス化
明示的な特殊化の構文は変更されました。特殊化を明示的に宣言したり、全部の定義をする場合は、宣言の前に template<> を付加してください (空の小なり括弧と大なり括弧が必要です)。たとえば、次のようにします。
// MyClass の特殊化 class MyClass<char>; // 古い形式の宣言 class MyClass<char> { ... }; // 古い形式の定義 template<> class MyClass<char>; // 標準の宣言 template<> class MyClass<char> { ... }; // 標準の定義
これらの形式は、引数のテンプレートに対してプログラマが異なる定義 (特殊化) をどこかで行なっていることを意味します。したがって、コンパイラは、これらの引数に対してはデフォルトのテンプレート定義を使用しません。
5.0 コンパイラの標準モードは、古い構文も旧式の構文として受け付けます。4.2 コンパイラは、新しい特殊化構文を受け付けますが、新しい構文を使用したコードをいつも正しく処理するとは限りません (この機能が 4.2 コンパイラに組み込まれた後に標準が変更されたため)。テンプレート特殊化コードの移植性を最大限に保つためには、プロジェクトのヘッダーファイルに次の例のような文を追加します。
#ifdef OLD_SPECIALIZATION_SYNTAX #define Specialize #else #define Specialize template<> #endif
Specialize class MyClass<char>; // 宣言
サンの C++ テンプレートは、テンプレートインスタンス用のレポジトリ (格納場所) を使用します。C++ 4.2 では、このレポジトリは、Templates.DB というディレクトリに置かれていました。Sun C++ 5.0 では、デフォルトでは、このディレクトリは SunWS_cache と SunWs_config です。SunWS_cashe には作業ファイルが含まれています。SunWS_config には、構成ファイル、特にテンプレートオプションファイル (SunWS_config/CC_tmp1_opt) が含まれています (『C++ ユーザーズガイド』を参照)。
何らかの理由でレポジトリ用のディレクトリの名前を指定したメークファイルがある場合は、手動で修正する必要があります。また、レポジトリの内部構造は変更されているため、Templates.DB の内容にアクセスするメークファイルを使用することはできなくなっています。
おそらく標準的な C++ プログラムは、従来と比べてテンプレートを頻繁に使用します。そのため、複数のプログラムやプロジェクトでディレクトリを共有する場合には注意が必要です。できれば最も簡単な構成にしてください。同じプログラムまたはライブラリに属するファイルは 1 つのディレクトリでコンパイルしなければなりません。これでテンプレートレポジトリは1つのプログラムに適用されます。同じディレクトリで別のプログラムをコンパイルする場合は、CCadmin -clean を使用して、レポジトリを事前に整理してください。詳細は、『C++ ユーザーズガイド』を参照してください。
複数のプログラムで同じディレクトリを共用すると、同じ名前に対して異なる定義が必要になる可能性があります。レポジトリを共有した場合、こうした状況に正しく対処することはできません。
C++ の標準ライブラリには、多数のテンプレートと、それらのテンプレートを使用するための多数の新しい標準ヘッダー名が含まれています。サンの C++ の標準ライブラリでは、テンプレートヘッダーに宣言が置かれ、標準ライブラリのテンプレートはそれぞれ別のファイルに置かれています。このため、プロジェクトファイル名に新しいテンプレートヘッダーと同じものがある場合は、誤ったテンプレートファイルが選択され、多数の意味不明のメッセージが出力される可能性があります。たとえば、ユーザーが vector というテンプレートを独自に作成していて、標準ライブラリのテンプレートは vector.cc というファイルに含まれているとしましょう。ファイルの位置とコマンド行オプションによっては、標準ライブラリの vector.cc が必要なときに、ユーザーが作成した vector.cc が選択されたり、その逆のことが起きたりする可能性があります。コンパイラの将来のリリースで export のようなキーワードが制定され、それを使用するテンプレートが実装された場合この状況はさらに悪くなります。
現在および将来こうした問題が発生するのを防ぐために、以下の 2 つのことをお勧めします。
独自のテンプレートファイル名に標準ヘッダー名を使用しない。
標準ライブラリはすべて名前空間の std に含まれているため、ユーザー作成のテンプレートやクラスと直接的に名前の衝突が起こることはありません。ただし、using 宣言や指令による間接的な衝突の可能性はあるため、標準ライブラリとのテンプレート名の重複は避けるのが賢明です。次に、テンプレートに関係する標準ヘッダーを示します。
algorithm bitset complex deque exception fstream functional iomanip ios iosfwd iostream istream iterator limits list locale map memory numeric ostream queue set sstream stack stdexcept streambuf string typeinfo utility valarray vector
標準ライブラリのテンプレートは、独立したファイルではなくヘッダーファイル (.h) に格納する。 こうすることで、標準ライブラリのファイル名の衝突を防ぐことができます。詳細については『C++ ユーザーズガイド』を参照してください。
C++ 標準では、クラスの名前がクラス自身に「挿入」されます。これは、以前の C++ 規則からの変更です。それまでは、クラス名はクラス中に名前としては入っていませんでした。
ほとんどの場合、この微妙な変更が既存のプログラムに影響することはありません。しかし場合によっては、この変更のために、それまで有効だったプログラムが無効になったり、意味が変わったりすることがあります。たとえば、次の場合がそうです。
const int X = 5; class X { int i; public: X(int j = X) : // X のデフォルト値は何か? i(j) { } };
デフォルトパラメータ値としての X の意味を判定するために、コンパイラは、名前 X を見つけるまで現在のスコープを探し、次にその外のスコープを次々に探します。
古い C++ 規則では、クラス X の名前はこのクラススコープにはありません。そのため、ファイルスコープの整数名 X によってクラス名 X が隠されてしまいます。したがって、デフォルト値として 5 が返されます。
新しい C++ 規則では、クラス X の名前がこのクラス自身の中にあります。コンパイラはクラスの中で X を見つけ、エラーを出します。コンパイラが見つける X は型名であって、整数値ではないためです。
同じスコープで同じ名前の型とオブジェクトを持つことはプログラミング手法として望ましくないため、このエラーはめったに起こらないはずです。このようなエラーになる場合は、次のように、変数を適切なスコープで修飾してください。
X(int j = ::X)
次の例は、スコープに関する別の問題です (標準ライブラリのコードを改造したもの)。
template class<T> class iterator { ... }; template class<T> class list { public: class iterator { ... }; class const_iterator : public ::iterator<T> { public: const_iterator(const iterator&); // どの反復子か };
const_iterator のコンストラクタに対するパラメータの型は何でしょうか。古い C++ 規則では、コンパイラは、クラス const_iterator のスコープに iterator という名前がないため、次の外側のスコープであるクラス list<T> を探します。次のスコープにはメンバー型として iterator があるため、パラメータの型は list<T>::iterator です。
新しい C++ 規則では、クラスの名前がそれ自身のスコープに挿入されます。具体的には、基底クラスの名前がその基底クラスに挿入されます。コンパイラは、派生クラスのスコープで名前を探し、基底クラスの名前を見つけます。const_iterator コンストラクタに対するパラメータの型にはスコープ修飾子がないため、その名前が const_iterator 基底クラスの名前です。したがって、パラメータの型は、list<T>::iterator ではなく、大域的な ::iterator<T> です。
目的の結果を得るには、いずれかの名前を変更するか、次のようにスコープ修飾子を使用してください。
const_iterator(const list<T>::iterator&);
ARM の規則では、for 文のヘッダーで宣言された変数は、for 文を含むスコープに挿入されると規定していました。しかし、C++ 委員会では、この規則は妥当ではなく、変数のスコープは for 文の終わりで終了すべきであると考えました。また、この規則が当てはまらない場合がいくつかあり、その結果として、コンパイラによって、コードの動作が異なるという事態も生じました。C++ 委員会が for 文中の変数に関する規則を変更したのは、こうした理由によります。ただし、C++ 4.2 コンパイラも含めて、多くのコンパイラでは、引き続き古い規則が採用されています。次の例の if 文は、古い規則では有効ですが、新しい規則では無効になります。これは、k がスコープ外にあるためです。
for( int k = 0; k < 10; ++k ) { ... } if( k == 10 ) ... // 有効か?
互換モードでは、C++ 5.0 コンパイラはデフォルトで古い規則を適用します。新しい規則の使用をコンパイラに指示するには、-features=localfor コンパイラオプションを使用してください。
標準モードでは、C++ 5.0 コンパイラはデフォルトで新しい規則を適用します。古い規則の使用をコンパイラに指示するには、-features=no%localfor コンパイラオプションを使用してください。
上記の for 文のヘッダーにある宣言を外に出すと、次の例のように、どのコンパイラのどのモードでも正しく動作するコードを作成することができます。
int k; for( k = 0; k < 10; ++k ) { ... } if( k == 10 ) ... // 常に有効なコード
標準 C++では文字列リテラルは const char[] 型として扱われ、char* と宣言された関数パラメータは文字列リテラルには渡されません。この変更の経緯を順を追って説明します。標準の C では、const キーワードと定数オブジェクトの概念が導入されました。これらのどちらも従来の C 言語 (K&R 形式の C) にはなかったものです。次の例に見られるような無意味な結果が出されないようにするには、論理的には「Hello world」などの文字列リテラルは const で宣言するべきです。
#define GREETING“Hello world”; char* greet = GREETING; // コンパイラからのエラー出力はない greet[0] = `J'; printf(“%s”, GREETING); // システムによっては「Jello world」と出力される
C、C++ とも、文字列リテラルを変更した結果がどうなるかは未定義です。同じ文字列リテラルに対し同じ書き込み可能記憶域を使用する実装の場合は、上の例のように奇妙な出力となります。
当時存在していたコードの多くが上記の例の 2 行目のようになっていたため、1989 年に C 標準委員会は文字列リテラルを const にはしませんでした。そのため、C++ 言語は当初 C 言語の規則に従いました。しかし、後日、C++ 標準委員会は、C++ においては型の安全性が重要と判断し、この文字列リテラルに関する規則を変更しました。
標準の C++ では、文字列リテラルは定数であり、const char[] 型です。上記の例の 2 行目は標準の C++ では無効です。同じように、char* で宣言した関数パラメータは、文字列リテラルとして渡すべきではありません。ところが C++ 標準では、文字列リテラル const char[] から char* への変換は不適切であると規定されています。この例をいくつか示します。
char *p1 =“Hello”; // 従来は問題なかったが、現在は不適切 const char* p2 =“Hello”; // OK void f(char*); f(p1); // p1 は const として宣言されていないので常に OK f(p2); // エラー、const char* を char* に渡している f(“Hello”); // 従来は問題なかったが、現在は不適切 void g(const char*); g(p1); // 常に OK g(p2); // 常に OK g(“Hello”); // 常に OK
引数として渡された文字配列が直接的にも間接的にも関数によって変更されることがない場合は、パラメータを const char* または const char[] と宣言してください。このようにすると、プログラムのいたるところで const 修飾子を追加する必要があることに気づくでしょう。修飾子を追加するほど、さらに多くの修飾子が必要になります (「const 中毒 (const poisoning)」と呼ばれることがある現象)。
標準モードの C++ 5.0 は、文字列リテラルから char* への変換が適切でないと警告を出します。妥当と思われるあらゆる場所に const を使用していれば、既存のプログラムは新しい規則でもおそらく変更なしにコンパイルされます。
関数を多重定義するために、標準モードでは、文字列リテラルは常に定数とみなされます。
void f(char*); void f(const char*); f(“Hello”); // どの f が呼び出されるか
上の例を互換モード (または 4.2 コンパイラ) でコンパイルすると、関数 f(char*) が呼び出されます。
標準モードでは、コンパイラは、リテラル文字列をデフォルトで読み取り専用記憶域に置きます。この文字列を変更しようとすると (char* への自動変換によって変更されることがある)、プログラムはメモリー違反で異常終了します。
次の例では、4.2 コンパイラも、互換モードの 5.0 コンパイラも、文字列リテラルを書き込み可能記憶域に置きます。プログラムは動作しますが、技術的にはその動作がどうなるかは未定義です。標準モードの 5.0 コンパイラは文字列リテラルをデフォルトで読み取り専用記憶域に置くため、プログラムはメモリー違反で異常終了します。そのため、文字列リテラルの変換に対するすべての警告に注意し、変換が起こらないようにプログラムを修正する必要があります。そうすれば、プログラムはどの C++ 実装でも正しく動作します。
void f(char* p) { p[0] = `J'; } int main() { f(“Hello”); // const char[] から char* への変換 }
コンパイラの動作は、コンパイラオプションを使って変更できます。
-features=conststrings コンパイラオプションを指定すると、コンパイラは、互換モードでも文字列リテラルを読み取り専用記憶域に置きます。
-features=no%conststrings コンパイラオプションを指定すると、コンパイラは、標準モードでも文字列リテラルを書き込み可能記憶域に置きます。
C 形式の文字列ではなく標準の C++ の string クラスを使用した方が便利なこともあります。標準の C++ の string オブジェクトは個別に const かどうか宣言したり、参照、ポインタ、値のどれによっても関数に渡せるため、string クラスには文字列リテラルに関係する問題はありません。
新しい形式の new と delete については、次の注意事項があります。
配列の形式
例外の指定
置き換え関数
ヘッダーファイル
互換モードでは、デフォルトで古い規則が適用されます。標準モードでは、デフォルトで新しい規則が適用されます。古い実行時ライブラリ (libC.so) は古い定義と動作に依存し、新しい標準ライブラリ (libCstd.so) は新しい定義と動作に依存するため、デフォルトを変更することはお勧めできません。
新しい規則を適用した場合、コンパイラは事前に _ARRAYNEW マクロを 1 に定義します。古い規則を適用した場合、このマクロは定義されません。次の使用例を参照してください。この意味については、次の節で詳しく説明します。
// 置き換え関数 #ifdef _ARRAYNEW void* operator new(size_t) throw(std::bad_alloc); void* operator new[](size_t) throw(std::bad_alloc); #else void* operator new(size_t); #endif
C++ 標準では、配列の割り当てあるいは割り当て解除を行うときに呼び出される operator new と operator delete の新しい形式が追加されています。従来は、これらの operator 関数は 1 つの形式しかありませんでした。また、配列の割り当てでは、大域形式の operator new と operator delete が使用され、クラス固有の形式は使用されませんでした。新しい形式を使用するには、ABI の変更が必要になるため、C++ 4.2 コンパイラでは、新しい形式はサポートされていません。
次の関数に加えて、
void* operator new(size_t);
void operator delete(void*);
C++ 標準では、以下の関数が追加されています。
void* operator new[](size_t);
void operator delete[](void*);
新旧いずれの場合も、実行時ライブラリにある形式とは別の形式を記述することができます。このように 2 つの形式が用意されているのは、配列と個々のオブジェクトに対して異なるメモリープールを使用できるようにするためと、配列に対してクラスが独自の形式の operator new を提供できるようにするためです。
新旧どちらの規則でも、new T と記述すると (T は特定の型)、operator new(size_t) 関数が呼び出されます。ただし、新しい規則で new T[n] と記述すると、operator new[](size_t) 関数が呼び出されます。
同様にどちらの規則でも delete p と記述すると、operator delete(void*) が呼び出されます。ただし、新しい規則で delete [] p; と記述すると、operator delete[](void*) が呼び出されます。
これらの関数について、クラス固有の配列形式を記述することもできます。
古い規則では、割り当てに失敗すると、どの形式の operator new でも NULL ポインタ を返します。新しい規則では、割り当てに失敗すると、通常の形式の operator new では例外を送出し、値は返しません。このほか、例外を送出する代わりにゼロを返す特殊な形式の operator new もあります。どの形式の operator new および operator delete にも、「例外指定」があります。次は、標準ヘッダーの <new> にある宣言です。
namespace std { class bad_alloc; struct nothrow_t {}; extern const nothrow_t nothrow; } // 単一オブジェクト形式 void* operator new(size_t size) throw(std::bad_alloc); void* operator new(size_t size, const std::nothrow_t&) throw(); void operator delete(void* ptr) throw(); void operator delete(void* ptr, const std::nothrow_t&) throw(); // 配列形式 void* operator new[](size_t size) throw(std::bad_alloc); void* operator new[](size_t size, const std::nothrow_t&) throw(); void operator delete[](void* ptr) throw(); void operator delete[](void* ptr, const std::nothrow_t&) throw();
次の例に示すような安全対策のためのコードは、新しい規則では意図したとおりには動作しません。割り当てに失敗すると、new 式から自動的に呼び出される operator new によって例外が送出され、ゼロを判定する検査は行われません。
T* p = new T; if( p == 0 ) { // 新しい規則ではエラー ... // 割り当て失敗の処理 } ... // pを使用する
このような場合には、次の 2 つの方法で解決できます。
以下のように、コードを記述し直して例外を捕獲できるようにする。
T* p = 0; try { p = new T; } catch( std::bad_alloc& ) { ... // 割り当て失敗の処理 } ... // pを使用する
コード中で例外を使用したくない場合は、2 番目の形式を使用してください。コード中で例外を使用するときは、最初の形式をお勧めします。
operator new が成功するかどうかを確認していない場合は、既存のコードを変更せずにそのまま使用してもかまいません。不正なメモリー参照が発生する箇所まで処理が進むことはなく、プログラムは割り当てに失敗した時点で異常終了します。
別の形式の operator new と operator delete を使用している場合、その関数は、例外の指定を含めて前述の例と同じ識別形式である必要があります。また、実装されている意味も同じである必要があります。通常の形式の operator new では、失敗時に bad_alloc 例外を送出する必要があります。これに対して nothrow 形式では、失敗時に例外を送出せずに、ゼロを返す必要があります。operator delete では、どの形式についても、例外を送出してはいけません。標準ライブラリのコードでは、大域的な operator new と operator delete が使用されており、コードが正しく実行されるかどうかは、その動作に依存します。他社のライブラリについても、同様の依存関係が存在する可能性があります。
C++ 5.0 の実行時ライブラリの大域形式の operator new[]() は、C++ 標準で規定されているように、単一オブジェクト形式の operator new() を呼び出すだけです。C++ 5.0 の標準ライブラリの大域形式の operator new() を置き換える場合、大域形式の operator new[]() を置き換える必要はありません。
C++ 標準では、あらかじめ定義されている、以下の「配置」形式の operator new の置き換えを禁止しています。
void* operator new(std::size_t, void*) throw();
void* operator new[](std::size_t, void*) throw();
上記の置き換えは、4.2 コンパイラでは許可されますが、C++ 5.0 標準モードでは置換できません。4.2コンパイラでは、別のパラメータリストを使用して独自の置き換えを記述することもできます。
互換モードの場合は、通常どおり <new.h> をインクルードしてください。標準モードでは、代わりに <new> (.h なし) をインクルードしてください。簡単に移行できるよう、標準モードでは、ヘッダーの <new.h> を使用すると、名前空間 std の名前を大域の名前空間が使用できます。このヘッダーには、例外の古い名前を新しい名前に対応させる typedef も用意されています。
ブール型 (bool、true、false) は、コンパイラで bool キーワードの認識が有効になっているかどうかによって制御されます。
互換モードでは、bool キーワードの認識はデフォルトで無効です。bool キーワードの認識を有効にするには、コンパイラオプション -features=bool を使用します。
標準モードでは、bool キーワードの認識はデフォルトで有効です。bool キーワードの認識を無効にするには、コンパイラオプション -features=no%bool を使用します。
互換モードでは、キーワードを有効にすることをお勧めします。これは、コード中でキーワードが現在どのように使用されているか明らかになるためです (注:既存のコードで使用されているブール型の定義に互換性があるとしても、実際の型が異なるため、名前の符号化に影響が生じます。その場合は、関数のパラメータにブール型を使用して、古いコードをすべて再コンパイルする必要があります)。
標準モードで bool キーワードを無効にすることは、お勧めしません。これは、C++ の標準ライブラリが、 bool 型に依存しているためです。後で bool を有効にすると、名前の符号化などのことで、さらに問題が生じます。
bool キーワードが有効な場合、コンパイラは、あらかじめ _BOOL マクロを 1 に定義します。キーワードが無効な場合、このマクロは定義されません。次に例を示します。
// 互換性のあるブール型の定義 #if !defined(_BOOL) && !defined(BOOL_TYPE) #define BOOL_TYPE // 局所インクルード対策 typedef unsigned char bool; // 標準モードでは、boolは1バイトを使用 const bool true = 1; const bool false = 0; #endif
互換モードでは、新しい組み込み型の bool 型とまったく同じように動作するブール型を定義することはできません。組み込み型の bool 型が C++ に追加されているのは、このためです。
関数は、次のような言語リンケージによって宣言できます。
extern“C”int f1(int);
リンケージを指定しないと、C++ のリンケージが使用されます。C++ リンケージは、明示的に指定することもできます。
extern“C++”int f2(int);
extern“C”{ int g1(); // C リンケージ int g2(); // C リンケージ int g3(); // C リンケージ } // セミコロンなし
「言語リンケージ」とは、関数の呼び出しに関する方法を意味します。たとえば、引数の場所、戻り値の検出場所の指定などがこれに当たります。言語リンケージを宣言するということは、その言語で関数が記述されないという意味です。言語リンケージを宣言すると、指定した言語で記述されているかのように関数を呼び出すことができます。つまり、C++ 関数が C リンケージを持つように宣言するとは、C 言語で記述された関数から C++ 関数を呼び出せるようにするということです。
関数の宣言に適用された言語リンケージは、戻り値型、および関数または関数へのポインタを持つすべてのパラメータに適用されます。
C++ 4.2 コンパイラでは、言語リンケージは関数の型の構成要素ではないという、ARM の規則が実装されています。特に、ポインタのリンケージや割り当てられた関数とは無関係に、関数へのポインタを宣言することができます。C++ 5.0 コンパイラの互換モードでは、これと同じ規則が適用されています。
C++ 5.0 の標準モードでは、言語リンケージはその関数の型の構成要素であり、かつ、関数へのポインタの型の構成要素であるという新しい規則が実装されています。このため、リンケージは関数とポインタとの間で一致していなければなりません。
次の例は、C リンケージと C++ リンケージを持つ関数および関数へのポインタの組み合わせとして考えられる 4 つの場合すべてを表しています。4.2 コンパイラおよび 5.0 コンパイラを互換モードで使用すると、あらゆる組み合わせが受け入れられます。標準モードの 5.0 コンパイラでは、一致していない組み合わせは旧式とみなされます。
extern“C”int fc(int) { return 1; } // fcの C リンケージ int fcpp(int) { return 1; } // fcppの C++ リンケージ // fp1 と fp2 の C++ リンケージ int (*fp1)(int) = fc; //不一致 int (*fp2)(int) = fcpp; // OK // fp3 と fp4 の C リンケージ extern“C”int (*fp3)(int) = fc; // OK extern“C”int (*fp4)(int) = fcpp; //不一致
リンケージに関連して問題が発生した場合は、C リンケージ関数と組み合わせ可能なポインタが C リンケージで宣言され、C++ リンケージ関数と組み合わせられるポインタにリンケージ指定がないか、または、C++ リンケージで宣言されていることを確認してください。
extern“C”{ int fc(int); int (*fp1)(int) = fc; // どちらも C リンケージを持つ } int fcpp(int); int (*fp2)(int) = fcpp; // どちらも C++ リンケージを持つ
ポインタと関数が一致しない場合は、関数を包含するコード (ラッパー) を記述することによって、コンパイラのエラーを回避することができます (Solaris では、C と C++ の関数リンケージは同じですが、一般的には、リンケージが一致していないとエラーになります。これは新しい言語規則として適用されているためです)。
次の例では、 composer は、C リンケージを持つ関数へのポインタを取る C 関数です。
extern“C”void composer( int(*)(int) ); extern“C++”int foo(int); composer( foo ); // 不一致
関数 foo (C++ リンケージを持つ) を 関数 composer に渡すには、次のように foo に C インタフェースを提供する foo_wrapper という C リンケージ関数を作成します。
extern“C”void composer( int(*)(int) ); extern“C++”int foo(int); extern“C”int foo_wrapper(int i) { return foo(i); } composer( foo_wrapper ); // OK
この手法は、コンパイラのエラーを回避するためだけでなく、C と C++ の関数が実際には異なるリンケージを持っている場合にも使用できます。
サンの実装している C と C++ の関数リンケージはバイナリ互換です。すべての C++ の実装がこうなっているわけではありませんが、比較的共通のことです。互換性がなくなってもかまわないのであれば、キャストを使って C++ リンケージ関数を C リンケージ関数と同じように使用できます。
たとえば静的メンバー関数がよい例です。リンケージに関する C++ 言語の新しい規則が関数の型の一部となるまでは、クラスの静的メンバー関数を C リンケージを持つ関数として扱うのが一般的でした。これによって、クラスメンバー関数のリンケージを宣言できないという制限を回避していました。たとえば、次の例を考えてみましょう。
// 既存のコード typedef int (*cfuncptr)(int); extern“C”void set_callback(cfuncptr); class T { ... static int memfunc(int); }; ... set_callback(T::memfunc); // 新しい規則では無効
上記の問題を解決するには、前の項でお勧めしたように T::memfunc を呼び出す関数ラッパーを作成してから、すべての set_callback 呼び出しを変更して T::memfunc の代りにラッパーを使用します。こうすると、完全な移植性を持つ正しいコードになります。
もう 1 つの解決策として、次の例のように多重定義した set_callback 呼び出しを作成して、C++ リンケージを持つ関数を受け取り、元の関数を呼び出すこともできます。
// 変更したコード extern“C”{ typedef int (*cfuncptr)(int); // C 関数へのポインタ void set_callback(cfuncptr); } typedef int (*cppfuncptr)(int); // C++ 関数へのポインタ inline void set_callback(cppfuncptr f) // 多重定義したもの { set_callback((cfuncptr)f); } class T { ... static int memfunc(int); }; ... set_callback(T::memfunc); // 元のコードと同じ
この例では、既存のコードをわずかに変更しただけです。ここには、コールバックを設定する set_callback を新たに追加しました。既存のコードは元の set_callback を呼び出していましたが、ここでは多重定義したものを呼び出し、それが元のものを呼び出します。多重定義したものはインライン関数なので、実行時のオーバーヘッドはまったくありません。
この方法は Sun C++ では動作しますが、すべての C++ の実装で動作するとは限りません。他のシステムでは、C 関数と C++ 関数の呼び出し順序が異なる場合があるからです。
言語リンケージに関する新しい規則の追加に伴う微妙な問題があります。それは、上記の例の composer 関数のような、パラメータとして関数へのポインタを取る関数の問題です。
extern“C”void composer( int(*)(int) );
言語リンケージに関する規則のうち、変更されていない規則として、言語リンケージを持つ関数が宣言されていて、その後に「同じ関数」が言語リンケージなしで定義されている場合は、前の言語リンケージが適用されるという規則があります。
extern“C”int f(int); int f(int i) { ... } // “C”リンケージを持つ
上記の関数 f は C リンケージを持ちます。この宣言 (インクルードされるヘッダーファイルに含まれている可能性もある) の後の定義は、リンケージ指定を継承します。しかし、次の例に示すように、この関数が関数へのポインタ型のパラメータを取る場合はどうなるのでしょう。
extern“C”int g( int(*)(int) ); int g( int(*pf)(int) ) { ... } //“C”または“C++”リンケージのどちらか?
古い規則と 4.2 コンパイラでは、このコードには、g という関数が 1 つ存在するだけです。新しい規則では、1 行目は、C リンケージを持つ関数へのポインタを取る、C リンケージを持つ関数 g を宣言し、2 行目は、C++ リンケージを持つ関数へのポインタを取る関数を定義していることになります。2 つの関数は同じではありません。2 つ目の関数は C++ リンケージを持ちます。リンケージは関数へのポインタの型の構成要素であるため、2 つの行は、それぞれが g という名前の多重定義関数を参照します。このため、これらの関数が同じ関数であることに依存するコードは、問題になります。コンパイルまたはリンクが失敗する可能性が非常に高くなります。
プログラミングするときの習慣として、リンケージは、宣言だけでなく、関数の定義でも指定するようにしてください。
extern“C”int g( int(*)(int) ); extern“C”int g( int(*pf)(int) ) { ... }
型に関する混乱は、関数パラメータに typedef を使用することでさらに少なくすることができます。
extern“C”typedef int (*pfc)(int); // Cリンケージ関数へのポインタ extern“C”int g(pfc); extern“C”int g(pfc pf) { ... }
4.2 コンパイラと同様に、5.0 の互換モードでは実行時の型識別 (RTTI) はデフォルトで無効です。標準モードでは、RTTI は有効であり、無効にすることはできません。古い ABI では、RTTI を有効にすると、データのサイズ、およびコンパイルの実行効率の面で著しい負担がかかっていました (古い ABI では、RTTI を直接に実装することができず、非効率的な間接的な方法を取る必要があったためです)。標準モードでは、新しい ABI を使用することにより、この負担は無視できるほどになっています (これは ABI で改善された機能の 1 つです)。
C++ 4.2 コンパイラには、C++ 標準の草案段階で提案されていた標準例外に関連する名前が、例外名として採用されています。それ以降、C++ 標準では、例外名が変更されてきました。C++ 5.0 の標準モードでは、標準の例外の名前として、次の表に示す名前が使用されています。
表 3-3 例外関連の型名
古い名前 |
標準名 |
説明 |
---|---|---|
xmsg |
exception |
標準例外の基底クラス |
xalloc |
bad_alloc |
割り当て要求の失敗で送出 |
terminate_function |
terminate_handler |
終了ハンドラ関数の型 |
unexpected_function |
unexpected_handler |
予期しない例外ハンドラ関数の型 |
クラスの使用方法が異なるように、こられクラスの公開メンバー (xmsg と exception および xalloc と bad_alloc) の使用方法は異なります。