C++ 移行ガイド ホーム目次前ページへ次ページへ索引


第 3 章

標準モードの使い方

この章では、Sun WorkShop 6 C++ コンパイラのデフォルトである標準モードの使い方について説明します。

標準モード

標準モードは、C++ コンパイラのデフォルトの動作モードです。このため、標準モードを指示するオプションを指定する必要はありません。指定する場合は、以下のオプションを使用してください。

-compat=5

例:

example% 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> をインクルードしても何の働きもしません) 。これらのトークンの意味は、次の表に示すとおりです。

表 3-2   トークンとトークン代替文字列
トークン 代替文字列
&& and
&&= and_eq
& bitand
~ compl
! not
!= not_eq
|| or
| bitor
~ xor
~= xor_eq


テンプレート

C++ 標準にはテンプレートに関する新しい規則がいくつか導入されています。そのため、既存のコードが標準から外れたものになってしまう可能性があります。特に、新しいキーワード typename を使用しているコードがこれに該当します。Sun WorkShop 6 C++ コンパイラでは、それらの規則はまだ強制はされていませんが、キーワード自体は認識されます。ほとんどの場合 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++ 標準では無効
T::V z; // 2. C++ 標準では無効
};

新しい言語規則では、テンプレート中の型名を解決するために、基底クラス名が自動的に検索されることはないと規定されています。また、キーワードの typename で宣言されていないかぎり、基底クラスやテンプレートパラメータクラスからとられた名前が型名になることはないとも規定されています。

上記の例の最初の無効な行 (1.) では、修飾クラス名とキーワードを使用せずに B から U を型として継承しようとしています。2 行目の無効な行 (2.) では、テンプレートパラメータからとられた型 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 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++ 標準と Sun WorkShop 6 C++ コンパイラの標準モードには、テンプレート定義を使って明示的にインスタンス化する構文 (キーワード 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> { ... }; // 標準の定義

これらの形式は、引数のテンプレートに対してプログラマが異なる定義 (特殊化) をどこかで行なっていることを意味します。したがって、コンパイラは、これらの引数に対してはデフォルトのテンプレート定義を使用しません。

コンパイラの標準モードは、古い構文も旧式の構文として受け付けます。4.2 コンパイラは、新しい特殊化構文を受け付けますが、新しい構文を使用したコードをいつも正しく処理するとは限りません (この機能が 4.2 コンパイラに組み込まれた後に標準が変更されたため)。テンプレート特殊化コードの移植性を最大限に保つためには、プロジェクトのヘッダーファイルに次の例のような文を追加します。

#ifdef OLD_SPECIALIZATION_SYNTAX
#define Specialize
#else
#define Specialize template<>
#endif

その上で、たとえば、次のような文を指定します。

Specialize class MyClass<char>; // 宣言

クラステンプレートの定義と宣言

クラステンプレートの定義と宣言では、山かっこ < > で囲まれた型引数が付いたクラスの名前は無効でしたが、バージョン 4 とバージョン 5.0 の C++ コンパイラはエラーを報告しませんでした。たとえば、次のコードでは、MyClass に付けられた <T> は定義と宣言のどちらでも無効です。

template<class T> class MyClass<T> { ... }; // 定義
template<class T> class MyClass<T>;         // 宣言

この問題を解決するには、次の例のように山かっこで囲まれた型引数をクラス名から削除します。

template<class T> class MyClass { ... }; // 定義
template<class T> class MyClass;         // 宣言

テンプレートレポジトリ (テンプレートの格納場所)

サンの C++ テンプレートは、テンプレートインスタンス用のレポジトリ (格納場所) を使用します。C++ 4.2 では、このレポジトリは、Templates.DB というディレクトリに置かれていました。Sun C++ 5.0 コンパイラおよび Sun WorkShop 6 C++ コンパイラでは、デフォルトでは、このディレクトリは 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 つのことをお勧めします。

クラス名の挿入

C++ 標準では、クラスの名前がクラス自身に「挿入」されます。これは、以前の C++ 規則からの変更です。それまでは、クラス名はクラス中に名前としては入っていませんでした。

ほとんどの場合、この微妙な変更が既存のプログラムに影響することはありません。しかし場合によっては、この変更のために、それまで有効だったプログラムが無効になったり、意味が変わったりすることがあります。たとえば、次の場合がそうです。

コード例 3-1   クラス名挿入の問題 1
const int X = 5;

 
class X {
    int i;
public:
    X(int j = X) : // X のデフォルト値は何か?
    i(j) { }
};

デフォルトパラメータ値としての X の意味を判定するために、コンパイラは、名前 X を見つけるまで現在のスコープを探し、次にその外のスコープを次々に探します。

同じスコープで同じ名前の型とオブジェクトを持つことはプログラミング手法として望ましくないため、このエラーはめったに起こらないはずです。このようなエラーになる場合は、次のように、変数を適切なスコープで修飾してください。

X(int j = ::X) 

次の例は、スコープに関する別の問題です (標準ライブラリのコードを改造したもの)。

コード例 3-2   クラス名挿入の問題 2
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&);

for 文中の変数

ARM の規則では、for 文のヘッダーで宣言された変数は、for 文を含むスコープに挿入されると規定していました。しかし、C++ 委員会では、この規則は妥当ではなく、変数のスコープは for 文の終わりで終了すべきであると考えました。また、この規則が当てはまらない場合がいくつかあり、その結果として、コンパイラによって、コードの動作が異なるという事態も生じました。C++ 委員会が for 文中の変数に関する規則を変更したのは、こうした理由によります。ただし、C++ 4.2 コンパイラも含めて、多くのコンパイラでは、引き続き古い規則が採用されています。次の例の if 文は、古い規則では有効ですが、新しい規則では無効になります。これは、k がスコープ外にあるためです。

for( int k = 0; k < 10; ++k ) {
...
}
if( k == 10 ) ... // 有効か?

互換モードでは、C++ コンパイラはデフォルトで古い規則を適用します。新しい規則の使用をコンパイラに指示するには、-features=localfor コンパイラオプションを使用してください。

標準モードでは、C++ コンパイラはデフォルトで新しい規則を適用します。古い規則の使用をコンパイラに指示するには、-features=no%localfor コンパイラオプションを使用してください。

上記の for 文のヘッダーにある宣言を外に出すと、次の例のように、どのコンパイラのどのモードでも正しく動作するコードを作成することができます。

int k;
for( k = 0; k < 10; ++k ) {
...
}
if( k == 10 ) ...
// 常に有効なコード

関数へのポインタと void* 間の変換

C++ コンパイラは互換モードと標準モードのどちらでも、+w2 オプションが指定された場合に、 関数へのポインタと void* 間での暗黙的および明示的変換に対して警告を出します。 コンパイラは、どちらのモードでも、多重定義された関数呼び出しを解釈処理するときには、暗黙的な変換を認識しません。詳細は、「関数ポインタと void*」を参照してください。

文字列リテラルと char*

標準 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); // システムによっては「Gello 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)」と呼ばれることがある現象)。

標準モードのコンパイラは、文字列リテラルから char* への変換が適切でないと警告を出します。妥当と思われるあらゆる場所に const を使用していれば、既存のプログラムは新しい規則でもおそらく変更なしにコンパイルされます。

関数を多重定義するために、標準モードでは、文字列リテラルは常に const とみなされます。

void f(char*);
void f(const char*);
f("Hello"); // どの f が呼び出されるか

上の例を互換モード (または 4.2 コンパイラ) でコンパイルすると、関数 f(char*) が呼び出されます。

標準モードでは、コンパイラは、リテラル文字列をデフォルトで読み取り専用記憶域に置きます。この文字列を変更しようとすると (char* への自動変換によって変更されることがある)、プログラムはメモリー違反で異常終了します。

次の例では、標準モードの C++ コンパイラも 4.2 コンパイラと同様に、文字列リテラルを書き込み可能記憶域に置きます。プログラムは動作しますが、技術的にはその動作がどうなるかは未定義です。標準モードのコンパイラは文字列リテラルをデフォルトで読み取り専用記憶域に置くため、プログラムはメモリー違反で異常終了します。そのため、文字列リテラルの変換に対するすべての警告に注意し、変換が起こらないようにプログラムを修正する必要があります。そうすれば、プログラムはどの C++ 実装でも正しく動作します。

void f(char* p) { p[0] = `J'; }
int main()
{
f("Hello"); // const char[] から char* への変換
}

コンパイラの動作は、コンパイラオプションを使って変更できます。

C 形式の文字列ではなく標準の C++ の string クラスを使用した方が便利なこともあります。標準の C++ の string オブジェクトは個別に const かどうか宣言したり、参照、ポインタ、値のどれによっても関数に渡せるため、string クラスには文字列リテラルに関係する問題はありません。

条件式

C++ 標準は条件式の規則に変更を導入しました。Sun WorkShop 6 C++ コンパイラは、標準モードと互換モードの両方で新しい規則を使用します。詳細は、「条件式」を参照してください。

新しい形式の new と delete

新しい形式の 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

new と delete の配列形式

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> にある宣言です。

コード例 3-3   標準ヘッダー <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 つの方法で解決できます。

コード中で例外を使用したくない場合は、2 番目の形式を使用してください。コード中で例外を使用するときは、最初の形式をお勧めします。

operator new が成功するかどうかを確認していない場合は、既存のコードを変更せずにそのまま使用してもかまいません。不正なメモリー参照が発生する箇所まで処理が進むことはなく、プログラムは割り当てに失敗した時点で異常終了します。

置き換え関数

別の形式の operator new と operator delete を使用している場合、その関数は、例外の指定を含めてコード例 3-3 と同じ識別形式である必要があります。また、実装されている意味も同じである必要があります。通常の形式の operator new では、失敗時に bad_alloc 例外を送出する必要があります。これに対して nothrow 形式では、失敗時に例外を送出せずに、ゼロを返す必要があります。operator delete では、どの形式についても、例外を送出してはいけません。標準ライブラリのコードでは、大域的な operator new と operator delete が使用されており、コードが正しく実行されるかどうかは、その動作に依存します。他社のライブラリについても、同様の依存関係が存在する可能性があります。

Sun WorkShop 6 C++ の実行時ライブラリの大域形式の operator new[]() は、C++ 標準で規定されているように、単一オブジェクト形式の operator new() を呼び出すだけです。Sun WorkShop 6 C++ の標準ライブラリの大域形式の 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 コンパイラでは許可されますが、標準モードでは置換できません。4.2コンパイラでは、別のパラメータリストを使用して独自の置き換えを記述することもできます。

インクルードするヘッダー

互換モードの場合は、通常どおり <new.h> をインクルードしてください。標準モードでは、代わりに <new> (.h なし) をインクルードしてください。簡単に移行できるよう、標準モードでは、ヘッダーの <new.h> を使用すると、名前空間 std の名前を大域の名前空間が使用できます。このヘッダーには、例外の古い名前を新しい名前に対応させる typedef も用意されています。

ブール型

ブール型 (bool、true、false) は、コンパイラで 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" 関数へのポインタ

関数は、次のような言語リンケージによって宣言できます。

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++ 関数を呼び出せるようにするということです。

関数の宣言に適用された言語リンケージは、戻り値型、および関数または関数へのポインタを持つすべてのパラメータに適用されます。

互換モードでは、言語リンケージは関数の型の構成要素ではないという、ARM の規則が実装されています。特に、ポインタのリンケージや割り当てられた関数とは無関係に、関数へのポインタを宣言することができます。これは、C++ 4.2 コンパイラと同じ動作です。

標準モードでは、言語リンケージはその関数の型の構成要素であり、かつ、関数へのポインタの型の構成要素であるという新しい規則が実装されています。このため、リンケージは関数とポインタとの間で一致していなければなりません。

次の例は、C リンケージと C++ リンケージを持つ関数および関数へのポインタの組み合わせとして考えられる 4 つの場合すべてを表しています。互換モードでは、コンパイラは 4.2 コンパイラと同じように、あらゆる組み合わせを受け入れます。標準モードのコンパイラでは、一致していない組み合わせは旧式とみなされます。

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) { ... }

実行時の型識別 (RTTI)

4.2 コンパイラと同様に、5.0 の互換モードでは実行時の型識別 (RTTI) はデフォルトで無効です。標準モードでは、RTTI は有効であり、無効にすることはできません。古い ABI では RTTI を有効にすると、データのサイズ、および実行効率の面で著しい負担がかかっていました (古い ABI では、RTTI を直接に実装することができず、非効率的な間接的な方法をとる必要があったためです)。標準モードでは、新しい ABI を使用することにより、この負担は無視できるほどになっています (これは ABI で改善された機能の 1 つです)。

標準の例外

C++ 4.2 コンパイラには、C++ 標準の草案段階で提案されていた標準例外に関連する名前が、例外名として採用されています。それ以降、C++ 標準では、例外名が変更されてきました。C++ 5.0 コンパイラおよび Sun WorkShop 6 C++ コンパイラ両方の標準モードでは標準の例外の名前として、次の表に示す名前が使用されています。

表 3-3   例外関連の型名
古い名前 標準名 説明
xmsg exception 標準例外の基底クラス
xalloc bad_alloc 割り当て要求の失敗で送出
terminate_function terminate_handler 終了ハンドラ関数の型
unexpected_function unexpected_handler 予期しない例外ハンドラ関数の型


クラスの使用方法が異なるように、こられクラスの公開メンバー (xmsg と exception および xalloc と bad_alloc) の使用方法は異なります。

静的オブジェクトの破棄の順序

「静的オブジェクト」とは、静的な記憶期間を持つオブジェクトのことです。静的オブジェクトは、大域オブジェクトでも名前空間中のオブジェクトでもかまいません。また、関数に局所的な静的変数でも、クラスの静的なデータメンバーでもかまいません。

C++ 標準は、静的オブジェクトの破棄は構築時とは逆の順序で行われるべきであると規定しています。さらに、atexit() 関数で登録された関数の破棄との兼ね合いについても規定しています。

以前のバージョンの Sun WorkShop C++ コンパイラは、1 つのモジュールで生成された大域静的オブジェクトを、生成時とは逆の順序で破棄していました。しかし、プログラム全体に渡って正しい順序で破棄されるかどうかは確約されてはいませんでした。

WorkShop 6 C++ コンパイラ以降、静的オブジェクトは、必ず構築されたときと逆の順序で破棄されます。たとえば、次のような型 T の静的オブジェクトが 3 つあると仮定します。

file1 と file2 にある 2 つの大域オブジェクトのどちらが最初に生成されるかどうかはわかりません。しかし、最初に生成された方の大域オブジェクトは、他方の大域オブジェクトが破棄された後に破棄されます。

局所静的オブジェクトは、その関数が呼び出されたときに生成されます。両方の大域静的オブジェクトが生成された後に関数が呼び出された場合、局所オブジェクトは、両方の大域オブジェクトが破棄される前に破棄されます。

C++ 標準は、atexit() 関数で登録された関数と静的オブジェクトの破棄との関連性について規定を追加しました。つまり、静的オブジェクト X が生成された後に関数 F が atexit() で登録された場合、F は、X が破棄される前に、プログラムの終了時に呼び出される必要があります。逆に言うと、X が生成される前に関数 F が atexit() で登録された場合、F は、X が破棄された後に、プログラムの終了時に呼び出される必要があります。

次に、この規則の例を示します。

// T はデストラクタを持つ型。
void bar();
void foo()
{
  static T t2;
  atexit(bar);
  static T t3;
}
T t1;
int main()
{
  foo();
}

プログラムの開始時には、t1 が生成され、その後で main が実行されます。main は foo() を呼び出します。foo() 関数は、次の作業をこの順序どおりに実行します。

  1. t2 を生成する。

  2. atexit() で bar() を登録する。

  3. t3 を生成する。

main の終了時には、exit が自動的に呼び出されます。終了手順は次の順序どおりに行われる必要があります。

  1. t3 を破棄する (t3 は bar() が atexit() で登録された後に生成された)。

  2. bar() を実行する。

  3. t2 を破棄する (t2 は bar() が atexit() で登録される前に生成された)。

  4. t1 を破棄する。t1 は最初に生成されたため、最後に破棄される。

このように静的デストラクタと atexit() 処理を交互に行うには、Solaris 実行時ライブラリ libc.so が必要です。この処理は Solaris 8 から実行できます。WorkShop 6 でコンパイルされた C++ プログラムは、実行時にライブラリ中で特別なシンボルを検索し、そのシンボルの有無から現在プログラムが動作しているバージョンの Solaris で上記の処理が実行できるかどうかを判断します。シンボルが存在する場合、静的デストラクタと atexit() で登録された関数は交互に処理されます。シンボルが存在しない場合、デストラクタは適切な順序で実行されますが、atexit() で登録された関数と関連付けて実行されることはありません。

この判断はプログラムが実行されるたびに行われます。プログラムが構築されたバージョンの Solaris は問題ではありません。現在プログラムが動作しているバージョンの Solaris が上記の処理をサポートしていて、Solaris 実行時ライブラリ libc.so が動的にリンクされていれば (デフォルトではリンクされる)、プログラム終了時に atexit() で登録された関数が実行されます。

静的オブジェクトの破棄の順序をどれだけ正しくサポートできるかは、コンパイラによって異なります。コードの移植性を向上させるには、静的オブジェクトが破棄される順序に影響を受けないようにプログラムを作成します。

プログラム中に破棄の順序に依存するコードがあり、かつ、古いコンパイラで作業する必要がある場合、標準モードでは、C++ 標準の規定によってプログラムが破壊されてしまう可能性があります。-features=no%strictdestrorder コマンドオプションを使用すると、厳密な破棄の順序を無効にできます。


サン・マイクロシステムズ株式会社
Copyright information. All rights reserved.
ホーム   |   目次   |   前ページへ   |   次ページへ   |   索引