この章では、このコンパイラ特有の言語拡張について説明します。この章で扱っている機能のなかには、コマンド行でコンパイラオプションを指定しないかぎり、コンパイラが認識しないものがあります。関連するコンパイラオプションは、各セクションに適宜記載します。
-features=extensions オプションを使用すると、ほかの C++ コンパイラで一般的に認められている非標準コードをコンパイルすることができます。このオプションは、不正なコードをコンパイルする必要があり、そのコードを変更することが認められていない場合に使用することができます。
この章では、-features=extensions オプションを使用した場合にサポートされる言語拡張について説明します。
不正なコードは、どのコンパイラでも受け入れられる有効なコードに簡単に変更することができます。コードの変更が認められている場合は、このオプションを使用する代わりに、コードを有効なものに変更してください。-features=extensions オプションを使用すると、コンパイラによっては受け入れられない不正なコードが残ることになります。
次の宣言指定子を、外部シンボルの宣言や定義の制約のために使用します。静的なアーカイブやオブジェクトファイルに対して指定したスコープは、共有ライブラリや実行可能ファイルにリンクされるまで、適用されません。しかしながら、コンパイラは、与えられたリンカースコープ指定子に応じたいくつかの最適化を行うことができます。
これらの指示子を使うと、リンカースコープのマップファイルは使用しなくてすみます。 -xldscope をコマンド行で指定することによって、変数スコープのデフォルト設定を制御することもできます。
詳細は、「A.2.136 -xldscope={v}」を参照してください。
表 4–1 リンカースコープ宣言指定子
より限定的な指定子を使ってシンボル定義を宣言しなおすことはできますが、より限定的でない指定子を使って宣言しなおすことはできません。シンボルは一度定義したら、異なる指示子で宣言することはできません。
__global はもっとも制限の少ないスコープです。__symbolic はより制限されたスコープです。__hidden はもっとも制限の多いスコープです。
仮想関数の宣言は仮想テーブルの構造と解釈に影響を及ぼすので、あらゆる仮想関数は、クラス定義を含んでいるあらゆるコンパイル単位から認識される必要があります。
C++ クラスでは、仮想テーブルや実行時型情報といった暗黙の情報の生成が必要と なることがあるため、 構造体、クラス、および共用体の宣言と定義にリンカースコープ指定子を適用できるようになっています。その場合、指定子は、構造体、クラス、または共用体キーワードの直後に置きます。こういったアプリケーションでは、すべての暗黙のメンバーに対して 1 つのリンカースコーピングが適用されます。
動的ライブラリに関して Microsoft Visual C++ (MSVC++) に含まれる類似のスコープ機能との互換性を保つため、次の構文もサポートされています。
__declspec(dllexport) は __symbolic と同一です。 |
__declspec(dllimport) は __global と同一です。 |
Sun C++ でこの構文の利点を活用するには、-xldscope=hidden オプションを CC コマンド行に追加するべきです。結果は、MSVC++ を使用する場合と比較可能なものになります。MSVC++ を使用する場合は、定義ではなく外部シンボルの宣言のみに関して __declspec(dllimport) が使用されることが想定されます。次に例を示します。
__declspec(dllimport) int foo(); // OK __declspec(dllimport) int bar() { ... } // not OK |
MSVC++ は、定義に対する dllimport の許容が緩やかであり、Sun C++ を使用する場合の結果と異なります。特に、Sun C++ で定義に対して dllimport を使用する場合の結果は、シンボルがシンボリックリンケージではなくグローバルリンケージを持つことになります。Microsoft Windows 上の動的ライブラリは、シンボルのグローバルリンケージをサポートしません。この問題が発生している場合は、定義に対して dllimport ではなく dllexport を使用するようにソースコードを変更できます。その後、MSVC++ と Sun C++ で同じ結果を得ることができます。
スレッドローカルの変数を宣言して、スレッドローカルな記憶領域を利用します。スレッドローカルな変数の宣言は、通常の変数宣言に宣言指定子 __thread を加えたものです。詳細については、「A.2.182 -xthreadvar[= o]」を参照してください。
__thread 指定子は、スレッド変数の最初の宣言部分に含める必要があります。__thread 指定子で宣言した変数は、__thread 指定子がない場合と同じように結合されます。
__thread 指定子で宣言できるのは、静的期間を持つ変数だけです。静的期間を持つ変数とは、ファイル内で大域なもの、ファイル内で静的なもの、関数内ローカルでかつ静的なもの、クラスの静的メンバなどが含まれます。期間が動的または自動である変数を __thread 指定子を使って宣言することは避けてください。スレッド変数に静的な初期設定子を持たせることはできますが、動的な初期設定子あるいはデストラクタを持たせることはできません。たとえば、__thread int x = 4; を使用することはできますが、__thread int x = f(); を使用することはできません。スレッド変数には、特殊なコンストラクタやデストラクタを持たせるべきではありません。とくに std::string 型をスレッド変数として持たせることはできません。
スレッド変数の演算子 (&) のアドレスは、実行時に評価され、現在のスレッドの変数のアドレスが返されます。したがって、スレッド変数のアドレスは定数ではありません。
スレッド変数のアドレスは、対応するスレッドの有効期間の間は安定しています。変数の有効期間内は、プロセス内の任意のスレッドがスレッド変数のアドレスを自由に使用できます。スレッドが終了したあとは、スレッド変数のアドレスを使用できません。スレッドの変数のアドレスは、スレッドが終了するとすべて無効となります。
C++ 標準では、関数を仮想関数で置き換える場合に、置き換える側の仮想関数で、置き換えられる側の関数より制限の少ない例外を指定することはできません。置き換える側の関数の例外指定は、置き換えられる側の関数と同じか、それよりも制限されている必要があります。例外指定がないと、あらゆる例外が認められてしまうことに注意してください。
たとえば、基底クラスのポインタを使用して関数を呼び出す場合を考えてみましょう。その関数に例外指定が含まれていれば、それ以外の例外が送出されることはありません。しかし、置き換える側の関数で、それよりも制限の少ない例外指定が定義されている場合は、予期しない例外が送出される可能性があり、その結果としてプログラムが異常終了することがあります。これが、前述の規則がある理由です。
-features=extensions オプションを使用すると、限定の少ない例外指定を含んだ関数による置き換えが認められます。
-features=extensions オプションを使用すると、コンパイラにより enum の型と変数の前方宣言が認められます。さらに、不完全な enum 型による変数宣言も認められます。不完全な enum 型は、現行のプラットフォームの int 型と同じサイズと範囲を持つと想定されます。
次の 2 つの行は、-features=extensions オプションを使用した場合にコンパイルされる不正なコードの例です。
enum E; // invalid: forward declaration of enum not allowed E e; // invalid: type E is incomplete |
enum 定義では、ほかの enum 定義を参照できず、ほかの型の相互参照もできないため、列挙型の前方宣言は必要ありません。コードを有効なものにするには、enum を使用する前に、その定義を完全なものにしておきます。
64 ビットアーキテクチャーでは、enum のサイズを int よりも大きくしなければならない場合があります。その場合に、前方宣言と定義が同じコンパイルの中で見つかると、コンパイラエラーが発生します。実際のサイズが想定されたサイズと異なっていて、コンパイラがそのことを検出できない場合は、コードのコンパイルとリンクは行われますが、実際のプログラムが正しく動作する保証はありません。特に、8 バイト値が 4 バイト変数に格納されると、プログラムの動作が不正になる可能性があります。
-features=extensions オプションを使用した場合は、不完全な enum 型は前方宣言と見なされます。たとえば、-features=extensions オプションを使用すると、次の不正なコードのコンパイルが可能になります。
typedef enum E F; // invalid, E is incomplete |
前述したように、enum 型を使用する前に、その定義を記述しておくことができます。
enum 宣言ではスコープを指定できないため、enum 名をスコープ修飾子として使用することはできません。たとえば、次のコードは不正です。
enum E {e1, e2, e3}; int i = E::e1; // invalid: E is not a scope name |
この不正なコードをコンパイルするには、-features=extensions オプションを使用します。enum 型の名前だった場合に、-features=extensions オプションはコンパイラにスコープ修飾子を無視するよう命令します。
このコードを有効なものにするには、不正な修飾子 E:: を取り除きます。
このオプションを使用すると、プログラムのタイプミスが検出されずにコンパイルされる可能性が高くなります。
名前のない構造体宣言は、構造体のタグも、オブジェクト名も、typedef 名も指定されていない宣言です。C++ では、名前のない構造体は認められていません。
-features=extensions オプションを使用すると、名前のない struct 宣言を使用できるようになります。ただし、この宣言は共用体のメンバーとしてだけ使用することができます。
次は、-features=extensions オプションを使用した場合にコンパイルが可能な、名前のない不正な struct 宣言の例です。
union U { struct { int a; double b; }; // invalid: anonymous struct struct { char* c; unsigned d; }; // invalid: anonymous struct }; |
これらの構造体のメンバー名は、構造体名で修飾しなくても認識されます。たとえば、共用体 U が前述のコードのように定義されているとすると、次のような記述が可能です。
U u; u.a = 1; |
名前のない構造体は、名前のない共用体と同じ制約を受けます。
コードを有効なものにするには、次のようにそれぞれの構造体に名前を付けます。
union U { struct { int a; double b; } A; struct { char* c; unsigned d; } B; }; U u; U.A.a = 1; |
一時変数のアドレスは取得できません。たとえば、次のコードは不正です。コンストラクタ呼び出しによって作成された変数のアドレスが取得されてしまうからです。ただし、-features=extensions オプションを使用した場合は、この不正なコードもコンパイル可能になります。
class C { public: C(int); ... }; void f1(C*); int main() { f1(&C(2)); // invalid } |
このコードを有効なものにするには、次のように明示的な変数を使用します。
C c(2); f1(&c); |
一時オブジェクトは、関数が終了したときに破棄されます。一時変数のアドレスを取得しないようにするのは、プログラムの作成者の責任になります。また、(f1 などで) 一時変数に格納されたデータは、その変数が破棄されたときに失われます。
次のコードは不正です。
class A { friend static void foo(<args>); ... }; |
クラス名に外部リンケージが含まれており、また、すべての定義が同一でなければならないため、フレンド関数にも外部リンケージが含まれている必要があります。しかし、-features=extensions オプションを使用すると、このコードもコンパイルできるようになります。
おそらく、この不正なコードの目的は、クラス A の実装ファイルに、メンバーではない「ヘルパー」関数を組み込むことでしょう。そうであれば、foo を静的メンバー関数にしても結果は同じです。クライアントから呼び出せないように、この関数を非公開にすることもできます。
この拡張機能を使用すると、作成したクラスを任意のクライアントが「横取り」できるようになります。そのためには、任意のクライアントにこのクラスのヘッダーを組み込み、独自の静的関数 foo を定義します。この関数は、自動的にこのクラスのフレンド関数になります。その結果は、このクラスのメンバーをすべて公開にした場合と同じになります。
-features=extensions オプションを使用すると、それぞれの関数で __func__ 識別子が const char 型の静的配列として暗黙的に宣言されます。プログラムの中で、この識別子が使用されていると、コンパイラによって次の定義が追加されます。ここで、function-name は関数の単純名です。この名前には、クラスメンバーシップ、名前空間、多重定義の情報は反映されません。
static const char __func__[] = "function-name"; |
たとえば、次のコードを考えてみましょう。
#include <stdio.h> void myfunc(void) { printf("%s\n", __func__); } |
この関数が呼び出されるたびに、標準出力ストリームに次の情報が出力されます。
myfunc |
struct または union の型定義に添付されるこの属性は、必要なメモリーを最小限に抑えるために、構造体または共用体の各メンバー (幅が 0 のビットフィールドを除く) の配置を指定します。enum 定義に添付する場合は、この属性は最小の整数型を使用することを示します。
struct および union 型に対してこの属性を指定する場合は、構造体または共用体の各メンバーに対して packed 属性を指定する場合と同じことを意味します。
次の例では、struct my_packed_struct のメンバーは互いに隣接してパックされますが、メンバーの内部レイアウトはパックされません。内部レイアウトをパックするには、struct my_unpacked_struct もパックする必要があります。
struct my_unpacked_struct { char c; int i; ; struct __attribute__ ((__packed__)) my_packed_struct { char c; int i; struct my_unpacked_struct s; }; |
この属性を指定できるのは、enum、 struct、または union の定義のみです。列挙型、構造体、共用体のいずれも定義しない typedef に対して、この属性は指定できません。
C++ プログラムのファイル編成は、C プログラムの場合よりも慎重に行う必要があります。この章では、ヘッダーファイルとテンプレート定義の設定方法について説明します。
有効なヘッダーファイルを簡単に作成できるとはかぎりません。場合によっては、C と C++ の複数のバージョンで使用可能なヘッダーファイルを作成する必要があります。また、テンプレートを使用するためには、複数回の包含 (べき等) が可能なヘッダーファイルが必要です。
場合によっては、C と C++ の両方のプログラムにインクルード可能なヘッダーファイルを作成する必要があります。ただし、従来の C とも呼ばれる Kernighan & Ritchie C (K&R C) や、ANSI C、『Annotated Reference Manual』C++ (ARM C++)、および ISO C++ では、1 つのヘッダーファイル内の同一のプログラム要素について異なった宣言や定義が規定されていることがあります。言語とバージョンによる違いについての詳細は、『C++ 移行ガイド』を参照してください。これらのどの標準言語でもヘッダーファイルで使用できるようにするには、プリプロセッサマクロ __STDC__ や __cplusplus の定義の有無またはその値に基づいた条件付きコンパイルを使用する必要があります。
__STDC__ マクロは、K&R C では定義されていませんが、ANSI C や C++ では定義されています。このマクロが定義されているかどうかを使用して、K&R C のコードを ANSI C や C++ のコードから区別します。このマクロは、プロトタイプの関数定義とプロトタイプではない関数定義を分離するときに特に役立ちます。
#ifdef __STDC__ int function(char*,...); // C++ & ANSI C declaration #else int function(); // K&R C #endif |
__cplusplus マクロは、C では定義されていませんが、C++ では定義されています。
旧バージョンの C++ では、__cplusplus の代わりに c_plusplus マクロが定義されていました。c_plusplus マクロは、現在のバージョンでは定義されていません。
__cplusplus マクロが定義されているかどうかを使用して、C と C++ を区別します。このマクロは、次のように関数宣言用の extern "C" インタフェースを保護するときに特に便利です。extern "C" の指定の一貫性を保つには、extern "C" のリンケージ指定のスコープ内には #include 指令を含めないでください。
#include “header.h” ... // ... other include files... #if defined(__cplusplus) extern “C” { #endif int g1(); int g2(); int g3() #if defined(__cplusplus) } #endif |
ARM C++ では、__cplusplus マクロの値は 1 です。ISO C++ では、このマクロの値は 199711L (long 定数で表現した、規格の年と月) です。この値の違いを使用して、ARM C++ と ISO C++ を区別します。これらのマクロ値は、テンプレート構文の違いを保護するときに特に役立ちます。
// template function specialization #if __cplusplus < 199711L int power(int,int); // ARM C++ #else template <> int power(int,int); // ISO C++ #endif |
ヘッダーファイルはべき等にしてください。すなわち、同じヘッダーファイルを何回インクルードしても、1 回だけインクルードした場合と効果が同じになるようにしてください。このことは、テンプレートでは特に重要です。べき等を実現するもっともよい方法は、プリプロセッサの条件を設定し、ヘッダーファイルの本体の重複を防止することです。
#ifndef HEADER_H #define HEADER_H /* contents of header file */ #endif |
テンプレート定義は 2 通りの方法で編成することができます。すなわち、テンプレート定義を取り込む方法 (定義取り込み型編成) と、分離する方法 (定義分離型編成) があります。テンプレート定義を取り込んだほうが、テンプレートのコンパイルを制御しやすくなります。
テンプレートの宣言と定義を、そのテンプレートを使用するファイルの中に含める場合、編成が定義の取り込みです。たとえば、次のようにします。
main.cc
template <class Number> Number twice(Number original); template <class Number> Number twice(Number original ) { return original + original; } int main() { return twice<int>(-3); } |
テンプレートを使用するファイルに、テンプレートの宣言と定義の両方を含んだファイルをインクルードした場合も、定義取り込み型編成を使用したことになります。たとえば、次のようにします。
twice.h
#ifndef TWICE_H #define TWICE_H template <class Number> Number twice(Number original); template <class Number> Number twice( Number original ) { return original + original; } #endif |
main.cc
#include “twice.h” int main() { return twice(-3); } |
テンプレートのヘッダーは、べき等にすることが特に重要です。「5.1.2 べき等ヘッダーファイル」を参照してください。
テンプレート定義を編成するもう一つの方法は、テンプレートの定義をテンプレート定義ファイルに記述することです。この例を次に示します。
twice.h
#ifndef TWICE_H #define TWICE_H template <class Number> Number twice(Number original); #endif TWICE_H |
twice.cc
template <class Number> Number twice( Number original ) { return original + original; } |
main.cc
#include “twice.h” int main( ) { return twice<int>( -3 ); } |
テンプレート定義ファイルには、べき等ではないヘッダーファイルをインクルードしてはいけません。また、通常はテンプレート定義ファイルにヘッダーファイルをインクルードする必要はありません。「5.1.2 べき等ヘッダーファイル」を参照してください。なお、テンプレートの定義分離型編成は、すべてのコンパイラでサポートされているわけではありません。
独立した定義ファイルはヘッダーファイルなので、多数のファイルに暗黙のうちにインクルードされることがあります。そのため、テンプレート定義の一部でないかぎり、あらゆる関数と変数はこのファイルに含めないようにします。独立した定義ファイルには、typedef などの型定義を定義できます。
通常、テンプレート定義ファイルには、ソースファイルの拡張子 (.c、.C、.cc、.cpp、.cxx、.c++ のいずれか) を付けますが、このテンプレート定義ファイルはヘッダーファイルです。コンパイラは、これらのファイルを必要に応じて自動的に取り込みます。テンプレート定義ファイルの単独コンパイルは行わないでください。
このように、テンプレートの宣言と定義を別々のファイルで指定した場合は、定義ファイルの内容、その名前、配置先に特に注意する必要があります。さらに、定義ファイルの配置先をコンパイラに明示的に通知する必要もあります。テンプレート定義の検索規則については、「7.5 テンプレート定義の検索」を参照してください。
テンプレートの目的は、プログラマが一度コードを書くだけで、そのコードが型の形式に準拠して広範囲の型に適用できるようにすることです。この章では関数テンプレートに関連したテンプレートの概念と用語を紹介し、より複雑な (そして、より強力な) クラステンプレートと、テンプレートの使用方法について説明しています。また、テンプレートのインスタンス化、デフォルトのテンプレートパラメータ、およびテンプレートの特殊化についても説明しています。この章の最後には、テンプレートの潜在的な問題が挙げられています。
関数テンプレートは、引数または戻り値の型だけが異なった、関連する複数の関数を記述したものです。
テンプレートは使用する前に宣言する必要があります。次の例に見られるように、宣言によってテンプレートを使用するのに十分な情報が提供されますが、テンプレートを実装するにはほかの情報も必要です。
template <class Number> Number twice( Number original ); |
この例では Number はテンプレートパラメータであり、テンプレートが記述する関数の範囲を指定します。つまり、Number はテンプレート型のパラメータです。テンプレート定義内で使用すると、型はテンプレートを使用するときに特定されることになります。
テンプレートは宣言と定義の両方が必要になります。テンプレートを定義することで、実装に必要な情報が得られます。次の例は、前述の例で宣言されたテンプレートを定義しています。
template <class Number> Number twice( Number original ) { return original + original; } |
テンプレート定義は通常ヘッダーファイルで行われるので、テンプレート定義が複数のコンパイル単位で繰り返される可能性があります。しかし、すべての定義は同じである必要があります。この制限は「単一定義ルール」と呼ばれています。
テンプレートは、いったん宣言するとほかのすべての関数と同様に使用することができます。テンプレートの使用は、そのテンプレートの命名と関数引数の提供で構成されます。コンパイラは、テンプレート型引数を、関数引数の型から推測します。たとえば、以前に宣言されたテンプレートを次のように使用できます。
double twicedouble( double item ) { return twice( item ); } |
テンプレート引数が関数の引数型から推測できない場合、その関数が呼び出される場所にその引数を指定する必要があります。たとえば、次のようにします。
template<class T> T func(); // no function arguments int k = func<int>(); // template argument supplied explicitly |
クラステンプレートは、複数の関連するクラスまたはデータ型を記述します。クラステンプレートに記述されているクラスは、型、整数値、大域リンケージによる変数へのポインタや参照だけが互いに異なっています。クラステンプレートは、一般的ではあるけれども型が保証されているデータ構造を記述するのに特に便利です。
クラステンプレートの宣言では、クラスの名前とそのテンプレート引数だけを指定します。このような宣言は「不完全なクラステンプレート」と呼ばれます。
次の例は、任意の型の引数をとる Array というクラスに対するテンプレート宣言の例です。
template <class Elem> class Array; |
次のテンプレートは、unsigned int の引数をとる String というクラスに対する宣言です。
template <unsigned Size> class String; |
クラステンプレートの定義では、次の例のようにクラスデータと関数メンバーを宣言する必要があります。
template <class Elem> class Array { Elem* data; int size; public: Array( int sz ); int GetSize(); Elem& operator[]( int idx ); }; |
template <unsigned Size> class String { char data[Size]; static int overflows; public: String( char *initial ); int length(); }; |
関数テンプレートとは違って、クラステンプレートには class Elem のような型パラメータと unsigned Size のような式パラメータの両方を指定できます。式パラメータには次の情報を指定できます。
整数型または列挙型を持つ値
オブジェクトへのポインタまたは参照
関数へのポインタまたは参照
クラスメンバー関数へのポインタ
クラステンプレートを完全に定義するには、その関数メンバーと静的データメンバーを定義する必要があります。動的 (静的でない) データメンバーの定義は、クラステンプレート宣言で十分です。
テンプレート関数メンバーの定義は、テンプレートパラメータの指定と、それに続く関数定義から構成されます。関数識別子は、クラステンプレートのクラス名とそのテンプレートの引数で修飾されます。次の例は、template <class Elem> というテンプレートパラメータ指定を持つ Array クラステンプレートの 2 つの関数メンバー定義を示しています。それぞれの関数識別子は、テンプレートクラス名とテンプレート引数 Array<Elem> で修飾されています。
template <class Elem> Array<Elem>::Array( int sz ) {size = sz; data = new Elem[size];} template <class Elem> int Array<Elem>::GetSize() { return size; } |
次の例は、String クラステンプレートの関数メンバーの定義を示しています。
#include <string.h> template <unsigned Size> int String<Size>::length( ) {int len = 0; while (len < Size && data[len]!= ’\0’) len++; return len;} template <unsigned Size> String<Size>::String(char *initial) {strncpy(data, initial, Size); if (length( ) == Size) overflows++;} |
テンプレートの静的データメンバーの定義は、テンプレートパラメータの指定と、それに続く変数定義から構成されます。この場合、変数識別子は、クラステンプレート名とそのテンプレートの実引数で修飾されます。
template <unsigned Size> int String<Size>::overflows = 0; |
テンプレートクラスは、型が使用できる場所ならどこででも使用できます。テンプレートクラスを指定するには、テンプレート名と引数の値を設定します。次の宣言例では、Array テンプレートに基づいた変数 int_array を作成します。この変数のクラス宣言とその一連のメソッドは、Elem が int に置き換わっている点以外は、Array テンプレートとまったく同じです (「6.3 テンプレートのインスタンス化」を参照)。
Array<int> int_array(100); |
次の宣言例は、String テンプレートを使用して short_string 変数を作成します。
String<8> short_string("hello"); |
テンプレートクラスのメンバー関数は、ほかのすべてのメンバー関数と同じように使用できます。
int x = int_array.GetSize( ); |
int x = short_string.length( ); . |
テンプレートのインスタンス化には、特定の組み合わせのテンプレート引数に対応した具体的なクラスまたは関数 (インスタンス) を生成することが含まれます。たとえば、コンパイラは Array<int> クラスと Array<double> に対応した別々のクラスを生成します。これらの新しいクラスの定義では、テンプレートクラスの定義の中のテンプレートパラメータがテンプレート引数に置き換えられます。前述の「クラステンプレート」の節に示す Array<int> の例では、Elem が表示されるたびに、コンパイラが int に置き換えられます。
テンプレート関数またはテンプレートクラスを使用すると、インスタンス化が必要になります。そのインスタンスがまだ存在していない場合には、コンパイラはテンプレート引数に対応したテンプレートを暗黙的にインスタンス化します。
コンパイラは、実際に使用されるテンプレート引数に対応したテンプレートだけを暗黙的にインスタンス化します。これは、テンプレートを持つライブラリの作成には適していない可能性があります。C++ には、次の例のように、テンプレートを明示的にインスタンス化するための手段が用意されています。
テンプレート関数を明示的にインスタンス化するには、template キーワードに続けて関数の宣言 (定義ではない) を行います。関数の宣言では関数識別子のあとにテンプレート引数を指定します。
template float twice<float>(float original); |
テンプレート引数は、コンパイラが推測できる場合は省略できます。
template int twice(int original); |
テンプレートクラスを明示的にインスタンス化するには、template キーワードに続けてクラスの宣言 (定義ではない) を行います。クラスの宣言ではクラス識別子のあとにテンプレート引数を指定します。
template class Array<char>; |
template class String<19>; |
クラスを明示的にインスタンス化すると、そのメンバーもすべてインスタンス化されます。
テンプレート関数メンバーを明示的にインスタンス化するには、template キーワードに続けて関数の宣言 (定義ではない) を行います。関数の宣言ではテンプレートクラスで修飾した関数識別子のあとにテンプレート引数を指定します。
template int Array<char>::GetSize(); |
template int String<19>::length(); |
テンプレートの静的データメンバーを明示的にインスタンス化するには、template キーワードに続けてメンバーの宣言 (定義ではない) を行います。メンバーの宣言では、テンプレートクラスで修飾したメンバー識別子のあとにテンプレート引数を指定します。
template int String<19>::overflows; |
テンプレートは、 入れ子にして使用できます。これは、標準 C++ ライブラリで行う場合のように、一般的なデータ構造に関する汎用関数を定義する場合に特に便利です。たとえば、テンプレート配列クラスに関して、テンプレートのソート関数を次のように宣言することができます。
template <class Elem> void sort(Array<Elem>); |
また、次のように定義することができます。
template <class Elem> void sort(Array<Elem> store) {int num_elems = store.GetSize(); for (int i = 0; i < num_elems-1; i++) for (int j = i+1; j < num_elems; j++) if (store[j-1] > store[j]) {Elem temp = store[j]; store[j] = store[j-1]; store[j-1] = temp;}} |
前述の例は、事前に宣言された Array クラステンプレートのオブジェクトに関するソート関数を定義しています。次の例はソート関数の実際の使用例を示しています。
Array<int> int_array(100); // construct an array of ints sort(int_array); // sort it |
クラステンプレートのテンプレートパラメータには、デフォルトの値を指定できます (関数テンプレートは不可)。
template <class Elem = int> class Array; template <unsigned Size = 100> class String; |
テンプレートパラメータにデフォルト値を指定する場合、それに続くパラメータもすべてデフォルト値である必要があります。テンプレートパラメータに指定できるデフォルト値は 1 つです。
次の twice の例のように、テンプレート引数を例外的に特定の形式で組み合わせると、パフォーマンスが大幅に改善されることがあります。あるいは、次の sort の例のように、テンプレート記述がある引数の組み合わせに対して適用できないこともあります。テンプレートの特殊化によって、実際のテンプレート引数の特定の組み合わせに対して代替実装を定義することが可能になります。テンプレートの特殊化はデフォルトのインスタンス化を無効にします。
前述のようなテンプレート引数の組み合わせを使用するには、その前に特殊化を宣言する必要があります。次の例は twice と sort の特殊化された実装を宣言しています。
template <> unsigned twice<unsigned>( unsigned original ); |
template <> sort<char*>(Array<char*> store); |
コンパイラがテンプレート引数を明確に確認できる場合には、次の例のようにテンプレート引数を省略することができます。たとえば、次のようにします。
template <> unsigned twice(unsigned original); |
template <> sort(Array<char*> store); |
宣言するテンプレートの特殊化はすべて定義する必要があります。次の例は、前の節で宣言された関数を定義しています。
template <> unsigned twice<unsigned>(unsigned original) {return original << 1;} |
#include <string.h> template <> void sort<char*>(Array<char*> store) {int num_elems = store.GetSize(); for (int i = 0; i < num_elems-1; i++) for (int j = i+1; j < num_elems; j++) if (strcmp(store[j-1], store[j]) > 0) {char *temp = store[j]; store[j] = store[j-1]; store[j-1] = temp;}} |
特殊化されたテンプレートはほかのすべてのテンプレートと同様に使用され、インスタンス化されます。ただし、完全に特殊化されたテンプレートの定義はインスタンス化でもあります。
前述の例では、テンプレートは完全に特殊化されています。つまり、このようなテンプレートは特定のテンプレート引数に対する実装を定義しています。テンプレートは部分的に特殊化することも可能です。これは、テンプレートパラメータの一部だけを指定する、または、1 つまたは複数のパラメータを特定のカテゴリの型に制限することを意味します。部分特殊化の結果、それ自身はまだテンプレートのままです。たとえば、次のコード例に、本来のテンプレートとそのテンプレートの完全特殊化を示します。
template<class T, class U> class A {...}; //primary template template<> class A<int, double> {...}; //specialization |
次のコード例に、本来のテンプレートの部分特殊化を示します。
template<class U> class A<int> {...}; // Example 1 template<class T, class U> class A<T*> {...}; // Example 2 template<class T> class A<T**, char> {...}; // Example 3 |
例 1 は、最初のテンプレートパラメータが int 型である特殊なテンプレート定義です。
例 2 は、最初のテンプレートパラメータが任意のポインタ型である、特殊なテンプレート定義です。
例 3 は、最初のテンプレートパラメータが任意の型のポインタへのポインタであり、2 番目のテンプレートパラメータが char 型である、特殊なテンプレート定義です。
この節では、テンプレートを使用する場合の問題について説明しています。
テンプレート定義で使用される名前の中には、テンプレート引数によって、またはそのテンプレート内で、定義されていないものがある可能性があります。そのような場合にはコンパイラが、定義の時点で、またはインスタンス化の時点で、テンプレートを取り囲むスコープから名前を解決します。1 つの名前が複数の場所で異なる意味を持つために解決の形式が異なることも考えられます。
名前の解決は複雑です。したがって、汎用性の高い標準的な環境で提供されているもの以外は、非局所型名前に依存することは避ける必要があります。言い換えれば、どこでも同じように宣言され、定義されている非局所型名前だけを使用するようにしてください。この例では、テンプレート関数の converter が、非局所型名前である intermediary と temporary を使用しています。これらの名前は use1.cc と use2.cc では異なる定義を持っているため、コンパイラが異なれば結果は違うものになるでしょう。テンプレートが正しく機能するためには、すべての非局所型名前 (intermediary と temporary) がどこでも同じ定義を持つ必要があります。
use_common.h // Common template definition template <class Source, class Target> Target converter(Source source) {temporary = (intermediary)source; return (Target)temporary;} use1.cc typedef int intermediary; int temporary; #include "use_common.h" use2.cc typedef double intermediary; unsigned int temporary; #include "use_common.h" |
非局所型名前を使用する典型的な例として、1 つのテンプレート内で cin と cout のストリームの使用があります。ほとんどのプログラマは実際、ストリームをテンプレートパラメータとして渡すことは望まないので、1 つの大域変数を参照するようにします。しかし、cin と cout はどこでも同じ定義を持っている必要があります。
テンプレートインスタンス化の際には、型と名前が一致することを目安に、どのテンプレートがインスタンス化または再インスタンス化される必要があるか決定されます。したがって、局所型がテンプレート引数として使用された場合には重大な問題が発生する可能性があります。自分のコードに同様の問題が生じないように注意してください。たとえば、次のようにします。
array.h template <class Type> class Array { Type* data; int size; public: Array(int sz); int GetSize(); }; array.cc template <class Type> Array<Type>::Array(int sz) {size = sz; data = new Type[size];} template <class Type> int Array<Type>::GetSize() {return size;} file1.cc #include "array.h" struct Foo {int data;}; Array<Foo> File1Data(10); file2.cc #include "array.h" struct Foo {double data;}; Array<Foo> File2Data(20); |
file1.cc に登録された Foo 型は、file2.cc に登録された Foo 型と同じではありません。局所型をこのように使用すると、エラーと予期しない結果が発生することがあります。
テンプレートは、使用前に宣言されている必要があります。フレンド宣言では、テンプレートを宣言するのではなく、テンプレートの使用を宣言します。フレンド宣言の前に、実際のテンプレートが宣言されている必要があります。次の例では、作成済みオブジェクトファイルをリンクしようとするときに、operator<< 関数が未定義であるというエラーが生成されます。その結果、operator<< 関数はインスタンス化されません。
array.h // generates undefined error for the operator<< function #ifndef ARRAY_H #define ARRAY_H #include <iosfwd> template<class T> class array { int size; public: array(); friend std::ostream& operator<<(std::ostream&, const array<T>&); }; #endif array.cc #include <stdlib.h> #include <iostream> template<class T> array<T>::array() {size = 1024;} template<class T> std::ostream& operator<<(std::ostream& out, const array<T>& rhs) {return out <<’[’ << rhs.size <<’]’;} main.cc #include <iostream> #include "array.h" int main() { std::cout << "creating an array of int... " << std::flush; array<int> foo; std::cout << "done\n"; std::cout << foo << std::endl; return 0; } |
コンパイラは、次の宣言を array クラスの friend である正規関数の宣言として読み取っているので、コンパイル中にエラーメッセージを表示しません。
friend ostream& operator<<(ostream&, const array<T>&); |
operator<< は実際にはテンプレート関数であるため、template class array を宣言する前にこの関数にテンプレート宣言を行う必要があります。しかし、operator<< はパラメータ type array<T> を持つため、関数宣言の前に array<T> を宣言する必要があります。ファイル array.h は、次のようになります。
#ifndef ARRAY_H #define ARRAY_H #include <iosfwd> // the next two lines declare operator<< as a template function template<class T> class array; template<class T> std::ostream& operator<<(std::ostream&, const array<T>&); template<class T> class array { int size; public: array(); friend std::ostream& operator<< <T> (std::ostream&, const array<T>&); }; #endif |
C++ 標準は、テンプレート引数に依存する修飾名を持つ型を、typename キーワードを使用して型名として明示的に示すことを規定しています。これは、それが型であることをコンパイラが認識できる場合も同様です。次の例の各コメントは、それぞれの修飾名が typename キーワードを必要とするかどうかを示しています。
struct simple { typedef int a_type; static int a_datum; }; int simple::a_datum = 0; // not a type template <class T> struct parametric { typedef T a_type; static T a_datum; }; template <class T> T parametric<T>::a_datum = 0; // not a type template <class T> struct example { static typename T::a_type variable1; // dependent static typename parametric<T>::a_type variable2; // dependent static simple::a_type variable3; // not dependent }; template <class T> typename T::a_type // dependent example<T>::variable1 = 0; // not a type template <class T> typename parametric<T>::a_type // dependent example<T>::variable2 = 0; // not a type template <class T> simple::a_type // not dependent example<T>::variable3 = 0; // not a type |
「>>」 という文字連続型は右シフト演算子と解釈されるため、あるテンプレート名を別のテンプレート名で使用する場合は注意が必要です。隣接する「>」文字を少なくとも 1 つの空白文字で区切ってください。
次に誤った書式の例を示します。
Array<String<10>> short_string_array(100); // >> = right-shift |
前述の文は、次のように解釈されます。
Array<String<10 >> short_string_array(100); |
正しい構文は次のとおりです。
Array<String<10> > short_string_array(100); |
テンプレート定義の内部では、大域スコープや名前空間で静的として宣言されたオブジェクトや関数の参照がサポートされません。複数のインスタンスが生成されると、それぞれのインスタンスが別々のオブジェクトを参照するため、一定義規約 (C++ 標準の第 3.2 節) に違反するためです。通常、このエラーはリンク時にシンボルの不足の形で通知されます。
すべてのテンプレートのインスタンス化で同じオブジェクトを共有する場合は、そのオブジェクトを該当する名前空間の非静的メンバーにします。また、あるテンプレートクラスをインスタンス化するたびに、別々のオブジェクトを使用する場合は、そのオブジェクトを該当するテンプレートクラスの静的メンバーにします。同様に、あるテンプレート関数をインスタンス化するたびに、別々のオブジェクトを使用する場合は、そのオブジェクトを該当するテンプレート関数の局所メンバーにします。
-instances=extern を指定して複数のプログラムまたはライブラリを構築する場合は、それらを別のディレクトリに構築することを推奨します。同一ディレクトリ内に構築する場合は、構築ごとにリポジトリを消去する必要があります。これにより、予期しないエラーが回避されます。詳細は、「7.4.4 テンプレートリポジトリの共有」を参照してください。
次のような各ファイルが存在する、次の例を考慮してください。make ファイル、 a.cc、b.cc、x.h、および x.cc この例は、-instances=extern を指定する場合にのみ意味があることに注意してください。
........ Makefile ........ CCC = CC all: a b a: $(CCC) -I. -instances=extern -c a.cc $(CCC) -instances=extern -o a a.o b: $(CCC) -I. -instances=extern -c b.cc $(CCC) -instances=extern -o b b.o clean: /bin/rm -rf SunWS_cache *.o a b |
... x.h ... template <class T> class X { public: int open(); int create(); static int variable; }; |
... x.cc ... template <class T> int X<T>::create() { return variable; } template <class T> int X<T>::open() { return variable; } template <class T> int X<T>::variable = 1; |
... a.cc ... #include "x.h" int main() { X<int> temp1; temp1.open(); temp1.create(); } |
... b.cc ... #include "x.h" int main() { X<int> temp1; temp1.create(); } |
a と b の両方を構築する場合は、それらの構築の間に make clean を実行します。次のコマンドでは、エラーが発生します。
example% make a example% make b |
次のコマンドでは、エラーは発生しません。
example% make a example% make clean example% make b |
テンプレートをコンパイルするためには、C++ コンパイラは従来の UNIX コンパイラよりも多くのことを行う必要があります。C++ コンパイラは、必要に応じてテンプレートインスタンスのオブジェクトコードを生成します。コンパイラは、テンプレートリポジトリを使って、別々のコンパイル間でテンプレートインスタンスを共有することができます。また、テンプレートコンパイルのいくつかのオプションを使用できます。コンパイラは、別々のソースファイルにあるテンプレート定義を見つけ、テンプレートインスタンスと main コード行の整合性を維持する必要があります。
フラグ -verbose=template が指定されている場合は、 テンプレートコンパイル作業中の重要なイベントがユーザーに通知されます。逆に、デフォルトの -verbose=no%template が指定されている場合は、コンパイラは通知しません。そのほかに、+w オプション を指定すると、テンプレートのインスタンス化が行われたときに問題になりそうな内容が通知される場合があります。
CCadmin(1) コマンドは、テンプレートリポジトリを管理します (-instances=extern オプションを使用する場合のみ)。たとえば、プログラムの変更によって、インスタンス化が不要になり、記憶領域が無駄になることがあります。CCadmin の -clean コマンド (以前のリリースの ptclean) を使用すれば、すべてのインスタンス化と関連データを整理できます。インスタンス化は、必要なときだけ再作成されます。
コンパイラは、テンプレートインスタンス生成のため、インラインテンプレート関数をインライン関数として扱います。コンパイラは、インラインテンプレート関数をほかのインライン関数と同じように管理します。この章の内容は、テンプレートインライン関数には適用されません。
コンパイラは通常、テンプレートクラスのメンバーをほかのメンバーからは独立してインスタンス化するので、プログラム内で使用されるメンバーだけがインスタンス化されます。デバッガによる使用を目的としたメソッドは、通常はインスタンス化されません。
デバッグ中のメンバーを、デバッガから確実に利用できるようにするということは、次の 2 つを行うことになります。
第 1 に、実際には使用されないテンプレートクラスインスタンスメンバーを使用する、非テンプレート関数を作成します。この関数は呼び出されないようにする必要があります。
第 2 に、-template=wholeclass コンパイラオプションを使用します。このオプションを指定すると、非テンプレートで非インラインのメンバーのうちのどれかがインスタンス化された場合に、ほかの非テンプレート、非インラインのメンバーもすべてインスタンス化されます。
ISO C++ 標準では、特定のテンプレート引用により、すべてのメンバーが正当であるとはかぎらないテンプレートクラスを作成してよいと規定しています。不正メンバーをインスタンス化しないかぎり、プログラムは依然として適正です。ISO C++ 標準ライブラリでは、この技法が使用されています。ただし、-template=wholeclass オプションはすべてのメンバーをインスタンス化するので、問題のあるテンプレート引数を使ってインスタンス化する場合には、この種のテンプレートクラスに使用できません。
インスタンス化とは、C++ コンパイラがテンプレートから使用可能な関数やオブジェクトを作成するプロセスをいいます。C++ コンパイラ ではコンパイル時にインスタンス化を行います。つまり、テンプレートへの参照がコンパイルされているときに、インスタンス化が行われます。
コンパイル時のインスタンス化の長所を次に示します。
デバッグが非常に簡単である。エラーメッセージがコンテキストの中に発生するので、コンパイラが参照位置を完全に追跡することができる。
テンプレートのインスタンス化が常に最新である。
リンク段階を含めて全コンパイル時間が短縮される。
ソースファイルが異なるディレクトリに存在する場合、またはテンプレートシンボルを指定してライブラリを使用した場合には、テンプレートが複数回にわたってインスタンス化されることがあります。
デフォルトでは、インスタンスは特別なアドレスセクションに移動し、リンカーは重複を認識、および破棄します。コンパイラには、インスタンスの配置とリンケージの方法として、外部、静的、大域、明示的、半明示的のどれを使うかを指定できます。
外部 インスタンスは、次の条件が成立する場合に最大のパフォーマンスを達成します。
プログラムに含まれているインスタンス全体は小さいが、各コンパイル単位がそれぞれ参照するインスタンスが大きい。
2、3 個以上のコンパイル単位で参照されるインスタンスがほとんどない。
デフォルトである大域インスタンスは、あらゆる開発に適していますが、さまざまなインスタンスをオブジェクトが参照する場合に最適です。
半明示的インスタンスは、前述より多少管理の程度が緩やかなアプリケーションコンパイル環境に適しています。ただし、このインスタンスは明示的インスタンスより大きなオブジェクトファイルを生成し、用途はかぎられています。
この節では、5 つのインスタンスの配置とリンケージの方法について説明します。インスタンスの生成に関する詳細は、「6.3 テンプレートのインスタンス化」にあります。
外部インスタンスの場合では、すべてのインスタンスがテンプレートリポジトリ内に置かれます。テンプレートインスタンスは 1 つしか存在できません。つまり、インスタンスが未定義であるとか、重複して定義されているということはありません。テンプレートは必要な場合にのみ再インスタンス化されます。非デバッグコードの場合、すべてのオブジェクトファイル (テンプレートキャッシュに入っているものを含む) の総サイズは、-instances=extern を指定したときの値が -instances=global を指定したときの値より小さくなることがあります。
テンプレートインスタンスは、リポジトリ内では大域リンケージを受け取ります。インスタンスは、現在のコンパイル単位からは、 外部リンケージで参照されます。
コンパイルとリンクを別々に実行し、コンパイル処理で -instance=extern を指定する場合は、リンク処理でも -instance=extern を指定する必要があります。
この方法にはキャッシュが壊れる恐れがあるという欠点があります。そのため、別のプログラムに替えたり、大幅な変更をプログラムに対して行なったりした場合にはキャッシュをクリアーする必要があります。キャッシュへのアクセスを一度に 1 回だけに限定しなければならないため、キャッシュは、dmake を使用する場合と同じように、並列コンパイルにおけるボトルネックとなります。また、1 つのディレクトリ内に構築できるプログラムは 1 個だけです。
メインオブジェクトファイル内にインスタンスを作成したあと必要に応じて破棄するよりも、有効なテンプレートインスタンスがすでにキャッシュに存在しているかどうかを確認するほうが、時間がかかる可能性があります。
外部リンケージは、-instances=extern オプションによって指定します。
インスタンスはテンプレートリポジトリ内に保存されているので、外部インスタンスを使用する C++ オブジェクトをプログラムにリンクするには CC コマンドを使用しなければなりません。
使用するすべてのテンプレートインスタンスを含むライブラリを作成したい場合には、-xar オプション でCC コマンドを使用してください。ar コマンドは使用できません。たとえば、次のようにします。
example% CC– xar -instances=extern– o libmain.a a.o b.o c.o |
詳細は、表 15–3を参照してください。
-instance=extern を指定する場合、キャッシュの衝突の可能性があるため、異なるバージョンのコンパイラを同一ディレクトリ内で実行しないでください。-instances=extern テンプレートモデルを使用する場合は、次の点に注意してください。
同一ディレクトリ内に、無関係のバイナリを作成しないでください。すべてのバイナリ (.o、a、.so、実行可能プログラム) は関連している必要があります。これは、複数のオブジェクトファイルに共通のすべてのオブジェクト、関数、型の名前は、定義が同一であるためです。
dmake を使用する場合などは、複数のコンパイルを同一ディレクトリで同時に実行しても問題はありません。ほかのリンク段階と同時にコンパイルまたはリンク段階を実行すると、問題が発生する場合があります。リンク段階とは、ライブラリまたは実行可能プログラムを作成する処理を意味します。メイクファイル内での依存により、1 つのリンク段階での並列実行が禁止されていることを確認してください。
-instances=static オプションは、非推奨です。-instances=global が static の利点をすべて備えており、かつ欠点を備えていないので、-instances=static を使用する理由はなくなっています。このオプションは、今はもう存在していない問題を克服するために、以前のバージョンで提供されました。
静的インスタンスの場合は、すべてのインスタンスが現在のコンパイル単位内に置かれます。その結果、テンプレートは各再コンパイル作業中に再インスタンス化されます。インスタンスはテンプレートリポジトリに保存されません。
この方法の欠点は、言語の意味解釈が規定どおりでないこと、かなり大きいオブジェクトと実行可能ファイルが作られることです。
インスタンスは静的リンケージを受け取ります 。これらのインスタンスは、現在のコンパイル単位以外では認識することも使用することもできません。そのため、テンプレートの同じインスタンス化がいくつかのオブジェクトファイルに存在することがあります。複数のインスタンスによって不必要に大きなプログラムが生成されるので、静的インスタンスのリンケージは、テンプレートがインスタンス化される回数が少ない小さなプログラムだけに適しています。
静的インスタンスは潜在的にコンパイル速度が速いため、修正継続機能を使用したデバッグにも適しています。『dbx コマンドによるデバッグ』を参照してください。
プログラムがコンパイル単位間で、テンプレートクラスまたはテンプレート機能の静的データメンバーなどのテンプレートインスタンスの共有に依存している場合は、静的インスタンス方式は使用しないでください。プログラムが正しく動作しなくなります。
静的インスタンスリンケージは、-instances=static コンパイルオプションで指定します。
旧リリースのコンパイラとは異なり、新リリースでは、大域インスタンスの複数のコピーを防ぐ必要はありません。
この方法の利点は、ほかのコンパイラで通常受け入れられる正しくないソースコードを、このモードで受け入れられるようになったという点です。特に、テンプレートインスタンスの中からの静的変数への参照は正当なものではありませんが、通常は受け入れられるものです。
この方法の欠点は、テンプレートインスタンスが複数のファイルにコピーされることから、個々のオブジェクトファイルが通常より大きくなる可能性がある点です。デバッグを目的としてオブジェクトファイルの一部を -g オプションを使ってコンパイルし、ほかのオブジェクトファイルを -g オプションなしでコンパイルした場合、プログラムにリンクされるテンプレートインスタンスが、デバッグバージョンと非デバッグバージョンのどちらであるかを予測することは難しくなります。
テンプレートインスタンスは大域リンケージを受け取ります。これらのインスタンスは、現在のコンパイル単位の外でも認識でき、使用できます。
大域インスタンスは、-instances= global オプションで指定します。これがデフォルトです。
明示的インスタンスの場合、インスタンスは、明示的にインスタンス化されたテンプレートに対してのみ生成されます。暗黙的なインスタンス化は行われません。インスタンスは現在のコンパイル単位に置かれます。
この方法の利点はテンプレートのコンパイル量もオブジェクトのサイズも、ほかのどの方法より小さくて済むことです。
欠点は、すべてのインスタンス化を手動で行う必要がある点です。
テンプレートインスタンスは大域リンケージを受け取ります。これらのインスタンスは、現在のコンパイル単位の外でも認識でき、使用できます。リンカーは、重複しているものを見つけ、破棄します。
明示的リンケージは、 -instances=explicit オプションによって指定します。
半明示的インスタンスの場合、インスタンスは、明示的にインスタンス化されるテンプレートやテンプレート本体の中で暗黙的にインスタンス化されるテンプレートに対してのみ生成されます。明示的に作成されるインスタンスが必要とするインスタンスは自動的に生成されます。main コード行内で行う暗黙的なインスタンス化は不完全になります。インスタンスは現在のコンパイル単位に置かれます。したがって、テンプレートは再コンパイルごとに再インスタンス化されます。インスタンスが大域リンケージを受けることはなく、テンプレートリポジトリには保存されません。
半明示的インスタンスは、-instances=semiexplicit オプションで指定します。
必要なときだけテンプレートインスタンスがコンパイルされるよう、コンパイルからコンパイルまでのテンプレートインスタンスが テンプレートリポジトリに保存されます。テンプレートリポジトリには、外部インスタンスメソッドを使用するときにテンプレートのインスタンス化に必要となる非ソースファイルがすべて入っています。このリポジトリがほかの種類のインスタンスに使用されることはありません。
テンプレートリポジトリは、デフォルトで、キャッシュディレクトリ (SunWS_cache) にあります。
キャッシュディレクトリは、オブジェクトファイルが置かれるのと同じディレクトリ内にあります。SUNWS_CACHE_NAME 環境変数を設定すれば、キャッシュディレクトリ名を変更できます。SUNWS_CACHE_NAME 変数の値は必ずディレクトリ名にし、パス名にしてはならない点に注意してください。これは、コンパイラが、テンプレートキャッシュディレクトリをオブジェクトファイルディレクトリの下に自動的に入れることから、コンパイラがすでにパスを持っているためです。
コンパイラは、テンプレートインスタンスを格納しなければならないとき、出力ファイルに対応するテンプレートリポジトリにそれらを保存します。たとえば、次のコマンド行では、オブジェクトファイルを ./sub/a.o に、テンプレートインスタンスを ./sub/SunWS_cache 内のリポジトリにそれぞれ書き込みます。コンパイラがテンプレートをインスタンス化するときにこのキャッシュディレクトリが存在しない場合は、このディレクトリが作成されます。
example% CC -o sub/a.o a.cc |
コンパイラは、読み込むオブジェクトファイルに対応するテンプレートリポジトリからテンプレートインスタンスを読み取ります。つまり、次のコマンド行は、/sub1/SunWS_cache と /sub2/SunWS_cache を読み取り、必要な場合は ./SunWS_cache に書き込みます。
example% CC sub1/a.o sub2/b.o |
リポジトリ内にあるテンプレートは、ISO/ANSI C++ 標準の単一定義規則に違反してはいけません。つまり、テンプレートは、どの用途に使用される場合でも、1 つのソースから派生したものでなければなりません。この規則に違反した場合の動作は定義されていません。
この規則に違反しないようにするための、もっとも保守的で、もっとも簡単な方法は、1 つのディレクトリ内では 1 つのプログラムまたはライブラリしか作成しないことです。無関係な 2 つのプログラムが同じ型名または外部名を使用して別のものを意味する場合があります。これらのプログラムがテンプレートリポジトリを共有すると、テンプレートの定義が競合し、予期せぬ結果が生じる可能性があります。
-instances=extern を指定すると、テンプレートリポジトリマネージャーは、リポジトリ中のインスタンスの状態をソースファイルと確実に一致させて最新の状態にします。
たとえば、ソースファイルが -g オプション (デバッグ付き) でコンパイルされる場合には、データベースの中の必要なファイルも -g でコンパイルされます。
さらに、テンプレートリポジトリはコンパイル時の変更を追跡します。たとえば、-DDEBUG フラグ を指定して名前 DEBUG を定義すると、データベースがこれを追跡します。その次のコンパイルでこのフラグを省くと、コンパイラはこの依存性が設定されているテンプレートを再度インスタンス化します。
テンプレートのソースコードを削除する場合や、テンプレートの使用を停止する場合も、テンプレートのインスタンスはキャッシュ内にとどまります。関数テンプレートの署名を変更する場合も、古い署名を使用しているインスタンスはキャッシュ内にとどまります。これらの課題が原因でコンパイル時またはリンク時に予期しない動作が発生した場合は、テンプレートキャッシュをクリアし、プログラムを再構築してください。
定義分離型テンプレートの編成、つまりテンプレートを使用するファイルの中にテンプレートの宣言だけがあって定義はないという編成を使用している場合には、現在のコンパイル単位にテンプレート定義が存在しないので、コンパイラが定義を検索しなければなりません。この節では、そうした検索について説明します。
定義の検索はかなり複雑で、エラーを発生しやすい傾向があります。このため、可能であれば、定義取り込み型のテンプレートファイルの編成を使用したほうがよいでしょう。こうすれば、定義検索をまったく行わなくて済みます。「5.2.1 テンプレート定義の取り込み」を参照してください。
-template=no%extdef オプションを使用する場合、コンパイラは分離されたソースファイルを検索しません。
オプションファイルで提供されるような特定の指令がない場合には、コンパイラは Cfront 形式の方法でテンプレート定義ファイルを検出します。この方法の場合、テンプレート宣言ファイルと同じベース名がテンプレート定義ファイルに含まれている必要があります。また、テンプレート定義ファイルが現在の include パス上に存在している必要もあります。たとえば、テンプレート関数 foo() が foo.h 内にある場合には、それと一致するテンプレート定義ファイルの名前を foo.cc か、またはほかの認識可能なソースファイル拡張子 (.C、.c、.cc、.cpp、.cxx、または .c++) にしなければなりません。テンプレート定義ファイルは、通常使用する include ディレクトリの 1 つか、またはそれと一致するヘッダーファイルと同じディレクトリの中に置かなければなりません。
-I で設定する通常の検索パスの代わりに、-ptidirectory オプションでテンプレート定義ファイルの検索ディレクトリを指定することができます。複数の -pti フラグは、複数の検索ディレクトリ、つまり 1 つの検索パスを定義します。-ptidirectory を使用している場合には、コンパイラはこのパス上のテンプレート定義ファイルを探し、-I フラグを無視します。しかし、-ptidirectory フラグはソースファイルの検索規則を複雑にするので、-ptidirectory オプションの代わりに -I オプションを使用してください。
コンパイラがコンパイル対象ではないファイルを検索するために、紛らわしい警告あるいはエラーメッセージが生成されることがあります。通常、問題は、たとえば foo.h というファイルにテンプレート宣言が含まれていて、foo.cc などの別のファイルが暗黙で取り込まれることにあります。
ヘッダーファイル foo.h の中にテンプレート宣言が存在する場合は、コンパイラはデフォルトで、foo という名前および C++ のファイル拡張子 (.C、.c、.cc、.cpp、.cxx、または .c++) を持つファイルをデフォルトで検索します。そうしたファイルを見つけた場合、コンパイラはそのファイルを自動的に取り込みます。こうした検索の詳細は、「7.5 テンプレート定義の検索」を参照してください。
このように扱われるべきでないファイル foo.cc が存在する場合、選択肢は 2 つあります。
.h または .cc の名前を変更して、名前が一致しないようにする。
-template=no%extdef オプションを指定することによって、テンプレート定義ファイルの自動検索を無効にする。この場合は、すべてのテンプレート定義をコードに明示的に取り込む必要があります。このため、「定義分離」モデルは使用できなくなります。
この章では、C++ コンパイラの例外処理の実装について説明します。「11.2 マルチスレッドプログラムでの例外の使用」にも補足情報を掲載しています。例外処理の詳細については、『プログラミング言語 C++』(第 3 版、Bjarne Stroustrup 著、アスキー、1997 年) を参照してください。
例外処理では、配列範囲のチェックといった同期例外だけがサポートされます。同期例外とは、例外を throw 文からだけ生成できることを意味します。
C++ 標準でサポートされる同期例外処理は、終了モデルに基づいています。終了とは、いったん例外が送出されると、例外の送出元に制御が二度と戻らないことを意味します。
例外処理では、キーボード割り込みなどの非同期例外の直接処理は行えません。ただし、注意して使用すれば、非同期イベントが発生したときに、例外処理を行わせることができます。たとえば、シグナルに対する例外処理を行うには、大域変数を設定するシグナルハンドラと、この変数の値を定期的にチェックし、値が変化したときに例外を送出するルーチンを作成します。シグナルハンドラから例外をスローすることはできません。
例外に関する実行時エラーメッセージには、次の 5 種類があります。
例外のハンドラがありません
予期しない例外を送出
ハンドラでは例外の再送出しかできません
スタックの巻き戻し中は、デストラクタは独自の例外を処理しなければなりません
メモリー不足
実行時にエラーが検出されると、現在の例外の種類と、前述の 5 つのメッセージのいずれかがエラーメッセージとして表示されます。デフォルト設定では、事前定義済みの terminate() 関数が呼び出され、さらにこの関数から abort() が呼び出されます。
コンパイラは、例外指定に含まれている情報に基づいて、コードの生成を最適化します。たとえば、例外を送出しない関数のテーブルエントリは抑止されます。また、関数の例外指定の実行時チェックは、できるかぎり省略されます。
プログラムで例外を使用しないことが明らかであれば、-features=no%except コンパイラオプションを使用して、例外処理用のコードの生成を抑止することができます。このオプションを使用すると、コードサイズが若干小さくなり、実行速度が多少高速になります。ただし、例外を無効にしてコンパイルしたファイルを、例外を使用するファイルにリンクすると、例外を無効にしてコンパイルしたファイルに含まれている局所オブジェクトが、例外が発生したときに破棄されずに残ってしまう可能性があります。デフォルト設定では、コンパイラは例外処理用のコードを生成します。時間と容量のオーバーヘッドが重要な場合を除いて、通常は例外を有効のままにしておいてください。
C++ 標準ライブラリ、dynamic_cast、デフォルトの new 演算子では例外が必要です。そのため、標準モード (デフォルトモード) でコンパイルを行う場合は、例外を無効にしないでください。
標準ヘッダー <exception> には、C++ 標準で指定されるクラスと例外関連関数が含まれています。このヘッダーは、標準モード (コンパイラのデフォルトモード、すなわち -compat=5 オプションを使用するモード) でコンパイルを行うときだけ使用されます。次は、<exception> ヘッダーファイル宣言を抜粋したものです。
// standard header <exception> namespace std { class exception { exception() throw(); exception(const exception&) throw(); exception& operator=(const exception&) throw(); virtual ~exception() throw(); virtual const char* what() const throw(); }; class bad_exception: public exception {...}; // Unexpected exception handling typedef void (*unexpected_handler)(); unexpected_handler set_unexpected(unexpected_handler) throw(); void unexpected(); // Termination handling typedef void (*terminate_handler)(); terminate_handler set_terminate(terminate_handler) throw(); void terminate(); bool uncaught_exception() throw(); } |
標準クラス exception は、構文要素や C++ 標準ライブラリから送出されるすべての例外のための基底クラスです。exception 型のオブジェクトは、例外を発生させることなく作成、複製、破棄することができます。仮想メンバー関数 what() は、例外についての情報を示す文字列を返します。
C++ release 4.2 で使用される例外との互換性について、ヘッダー <exception.h> は標準モードでの使用のため提供されます。このヘッダーは、C++ 標準のコードに移行するためのもので、C++ 標準には含まれていない宣言を含んでいます。開発スケジュールに余裕があれば、<exception.h> の代わりに <exception> を使用し、コードを C++ 標準に従って更新してください。
// header <exception.h>, used for transition #include <exception> #include <new> using std::exception; using std::bad_exception; using std::set_unexpected; using std::unexpected; using std::set_terminate; using std::terminate; typedef std::exception xmsg; typedef std::bad_exception xunexpected; typedef std::bad_alloc xalloc; |
互換モード (—compat[=4]) では、ヘッダー <exception> は使用できません。また、ヘッダー <exception.h> は、C++ release 4.2 で提供されているヘッダーと同じですので、ここでは再生されません。
同じプログラムの中で、setjmp/longjmp 関数と例外処理を併用することができます。ただし、これらが相互に干渉しないことが条件になります。
その場合、例外と setjmp/longjmp のすべての使用規則が、それぞれ別々に適用されます。また、A 地点から B 地点への longjmp を使用できるのは、例外を A 地点から送出し、B 地点で捕獲した場合と効果が同じになる場合だけです。特に、try ブロック (または catch ブロック) への、または try ブロック (または catch ブロック) からの、直接的または間接的な longjmp や、自動変数や一時変数の初期化や明示的な破棄の前後にまたがる longjmp は行なってはいけません。
シグナルハンドラからは例外を送出できません。
C++ コードを含むプログラムは -Bsymbolic を使用せず、代わりにリンカーマップファイルまたはリンカースコープオプションを使用してください (「4.1 リンカースコープ」 を参照)。-Bsymbolic を使用すると、異なるモジュール内の参照が、本来 1 つの大域オブジェクトの複数の異なる複製に結合されてしまう可能性があります。
例外メカニズムは、アドレスの比較によって機能します。オブジェクトの複製が 2 つある場合は、アドレスが同一であると評価されず、本来一意のアドレスを比較することで機能する例外メカニズムで問題が発生することがあります。
この章では、C++ 標準の新しいキャスト演算子、すなわち const_cast、reinterpret_cast、static_cast、dynamic_cast について説明します。キャストとは、オブジェクトや値の型を、別の型に変換することです。
これらのキャスト演算子を使用すると、従来のキャスト演算子よりも緻密な制御を行うことができます。たとえば、dynamic_cast<> 演算子では、多相クラスのポインタの実際の型を確認することができます。新形式のキャストには、_cast を検索することで、テキストエディタで簡単に検出できるという利点もあります。従来のキャストは、構文チェックを行わないと検出できません。
新しいキャストは、それぞれ従来のキャスト表記で行うことのできる各種のキャスト操作の一部だけを実行します。たとえば、const_cast<int*>(v) は、従来であれば (int*)v と記述することができます。新しいキャストは、コードの意図をより明確に表現し、コンパイラがより的確なチェックを行えるように、実行可能な各種のキャスト操作を単に類別したものです。
キャスト演算子は常に有効になります。これらを無効にすることはできません。
式 const_cast<T >(v) を使用して、ポインタまたは参照の const 修飾子または volatile 修飾子を変更することができます。新しい形式のキャストのうち、const 修飾子を削除できるのは const_cast<> のみです。T はポインタ、参照、またはメンバー型へのポインタである必要があります。
class A { public: virtual void f(); int i; }; extern const volatile int* cvip; extern int* ip; void use_of_const_cast() { const A a1; const_cast<A&>(a1).f(); // remove const ip = const_cast<int*> (cvip); // remove const and volatile } |
式 reinterpret_cast<T >(v) は式 v の値の解釈を変更します。この式は、ポインタ型と整数型の間、2 つの無関係なポインタ型の間、ポインタ型からメンバー型へ、ポインタ型から関数型へ、という各種の変換に使用できます。
reinterpret_cast 演算子を使用すると、未定義の結果または実装に依存しない結果を出すことがあります。次に、確実な動作について説明します。
データオブジェクトまたは関数へのポインタ (メンバーへのポインタは除く) は、それを十分保持できる大きさの任意の整数型に変換できます。long 型は十分大きいため、C++ コンパイラがサポートするアーキテクチャーでは常にポインタ値を保持できます。元の型に戻しても、値は元の値と同じになります。
(非メンバー) 関数へのポインタは、別の (非メンバー) 関数型へのポインタに変換できます。元の型に戻しても、値は元の値と同じになります。
新しい型が元の型よりも厳しい整列条件を持たない場合、オブジェクトへのポインタは別のオブジェクト型へのポインタに変換できます。元の型に戻しても、値は元の値と同じになります。
reinterpret_cast 演算子を使用して型「T1 のポインタ」の式を型「T2 のポインタ」に変換できる場合、型 T1 の左辺値は型「T2 の参照」に変換できます。
T1 と T2 の両方が関数型であるか両方がオブジェクト型である場合、「型 T1 の Xのメンバーを指すポインタ」型の右辺値は、「型 T2 の Y のメンバーを指すポインタ」型の右辺値に明示的に変換できます。
変換が許可されている場合は、ある型のヌルポインタは別の型のヌルポインタに変換されたあともヌルポインタのままです。
reinterpret_cast 演算子を使用して、const を const でない型にキャストすることはできません。このようにキャストするには const_cast を使用します。
reinterpret_cast 演算子を使用して、ポインタと、同じクラス階層に存在する別のクラスの間の変換を行うことはできません。このように変換するには、静的キャストまたは動的キャストを使用します。reinterpret_cast は、必要があっても調整は行いません。これを次の例で説明します。
class A {int a; public: A();}; class B: public A {int b, c;}; void use_of_reinterpret_cast() { A a1; long l = reinterpret_cast<long>(&a1); A* ap = reinterpret_cast<A*>(l); // safe B* bp = reinterpret_cast<B*>(&a1); // unsafe const A a2; ap = reinterpret_cast<A*>(&a2); // error, const removed } |
式 static_cast<T >(v) は、式の値 v を型 T の値に変換します。この式は、暗黙的に実行されるすべての型変換に使用できます。さらに、いかなる値でも void にキャストすることができ、いかなる暗黙的型変換でも、そのキャストが旧式のキャストと同様に正当であるかぎり、反転させることができます。
class B {...}; class C: public B {...}; enum E {first=1, second=2, third=3}; void use_of_static_cast(C* c1) { B* bp = c1; // implicit conversion C* c2 = static_cast<C*>(bp); // reverse implicit conversion int i = second; // implicit conversion E e = static_cast<E>(i); // reverse implicit conversion } |
static_cast 演算子を使用して、const を const 以外の型にするようなキャストを行うことはできません。階層の下位に (基底から派生ポインタまたは参照へ) キャストするには static_cast を使用できますが、変換は検証されず、結果は使用できない場合があります。抽象基底クラスから下位へのキャストには、static_cast は使用できません。
クラスへのポインタまたは参照は、そのクラスから派生されたすべてのクラスを実際に指す (参照する) ことができます。場合によっては、オブジェクトの完全派生クラス、またはその完全なオブジェクトのほかのサブオブジェクトへのポインタを得る方が望ましいことがあります。動的キャストによってこれが可能になります。
互換モード (-compat[=4]) でコンパイルする場合、プログラムが動的キャストを使用している場合は、-features=rtti を付けてコンパイルする必要があります。
動的な型のキャストは、あるクラス T1 へのポインタまたは参照を、別のクラス T2 のポインタまたは参照に変換します。T1 とT2 は、同じ階層内にある必要があります。両クラスとも (公開派生を介して) アクセス可能でなければならず、変換はあいまいであってはいけません。また、変換が派生クラスからその基底クラスの 1 つに対するものでないかぎり、T1 と T2 の両方が入った階層の最小の部分は多相性がある必要があります (少なくとも仮想関数が 1 つ存在すること)。
式 dynamic_cast<T>(v) では、v はキャストされる式であり、T はキャストの対象となる型です。T は完全なクラス型 (定義が参照できるもの) へのポインタまたは参照であるか、あるいは「cv void へのポインタ」である必要があります。ここで cv は空の文字列、const、volatile、const volatile のいずれかです。
階層の上位にキャストする場合で、v が指す (参照する) 型の基底クラスを T が指す(あるいは参照する) 場合、変換は static_cast<T>(v) で行われるものと同じです。
T が void* の場合、結果はオブジェクト全体のポインタになります。つまり、v はあるオブジェクト全体の基底クラスの 1 つを指す可能性があります。この場合、dynamic_cast<void*>(v) の結果は、v をオブジェクト全体の型 (種類は問わない) に変換したあとで void* に変換した場合と同じです。
void* にキャストする場合、階層に多相性がある必要があります (仮想関数が存在すること)。結果は実行時に検証されます。
階層の下位または全体にキャストする場合、階層に多相性がある必要があります (仮想関数を持つ必要がある)。結果は実行時に検証されます。
階層の下位または全体にキャストする場合、v から T に変換できないことがあります。たとえば、試行された変換があいまいであったり、T に対するアクセスが不可能であったり、あるいは必要な型のオブジェクトを v が指さない (あるいは参照しない) 場合がこれに当たります。実行時検査に失敗し、T がポインタ型である場合、キャスト式の値は型 T のヌルポインタです。T が参照型の場合、何も返されず (C++ にはヌル参照は存在しない)、標準例外 std::bad_cast が送出されます。
たとえば、次の公開派生のコード例は正常に実行されます。
#include <assert.h> #include <stddef.h> // for NULL class A {public: virtual void f();}; class B {public: virtual void g();}; class AB: public virtual A, public B {}; void simple_dynamic_casts() { AB ab; B* bp = &ab; // no casts needed A* ap = &ab; AB& abr = dynamic_cast<AB&>(*bp); // succeeds ap = dynamic_cast<A*>(bp); assert(ap!= NULL); bp = dynamic_cast<B*>(ap); assert(bp!= NULL); ap = dynamic_cast<A*>(&abr); assert(ap!= NULL); bp = dynamic_cast<B*>(&abr); assert(bp!= NULL); } |
これに対して、次のコード例は正しく実行されません。基底クラス B にアクセスできないからです。
#include <assert.h> #include <stddef.h> // for NULL #include <typeinfo> class A {public: virtual void f() {}}; class B {public: virtual void g() {}}; class AB: public virtual A, private B {}; void attempted_casts() { AB ab; B* bp = (B*)&ab; // C-style cast needed to break protection A* ap = dynamic_cast<A*>(bp); // fails, B is inaccessible assert(ap == NULL); try { AB& abr = dynamic_cast<AB&>(*bp); // fails, B is inaccessible } catch(const std::bad_cast&) { return; // failed reference cast caught here } assert(0); // should not get here } |
1 つの基底クラスについて仮想継承と多重継承が存在する場合には、実際の動的キャストは一意の照合を識別することができる必要があります。もし照合が一意でないならば、そのキャストは失敗します。たとえば、次の追加クラス定義が与えられたとします。
class AB_B: public AB, public B {}; class AB_B__AB: public AB_B, public AB {}; |
次に例を示します。
void complex_dynamic_casts() { AB_B__AB ab_b__ab; A*ap = &ab_b__ab; // okay: finds unique A statically AB*abp = dynamic_cast<AB*>(ap); // fails: ambiguous assert(abp == NULL); // STATIC ERROR: AB_B* ab_bp = (AB_B*)ap; // not a dynamic cast AB_B*ab_bp = dynamic_cast<AB_B*>(ap); // dynamic one is okay assert(ab_bp!= NULL); } |
dynamic_cast のエラー時のヌル (NULL) ポインタの戻り値は、コード中の 2 つのブロック (1 つは型推定が正しい場合にキャストを処理するためのもの、もう 1 つは正しくない場合のもの) の間の条件として役立ちます。
void using_dynamic_cast(A* ap) { if (AB *abp = dynamic_cast<AB*>(ap)) { // abp is non-null, // so ap was a pointer to an AB object // go ahead and use abp process_AB(abp);} else { // abp is null, // so ap was NOT a pointer to an AB object // do not use abp process_not_AB(ap); } } |
互換モード (-compat[=4]) では、-features=rtti コンパイラオプションによって実行時の型情報が有効になっていないと、コンパイラは dynamic_cast を static_cast に変換し、警告メッセージを出します。
実行時型情報が無効にされている場合、コンパイラは dynamic_cast<T&> を static_cast<T&> に変換し、警告を発行します。参照型への動的キャストを行う場合は、そのキャストが実行時に無効であると判明したときに送出される例外が必要です例外の詳細については、「7.5.3 問題がある検索の回避」を参照してください。
動的キャストは必然的に、仮想関数による変換のような適切な設計パターンより遅くなります。Erich Gamma 著 (ソフトバンク)『オブジェクト指向における再利用のためのデザインパターン』を参照してください。
C++ 関数のパフォーマンスを高めるには、コンパイラが C++ 関数を最適化しやすいように関数を記述することが必要です。全般的、および C++ のソフトウェアパフォーマンスに関して、多くの書物が制作されてきたため、この章ではそのような重要な情報を繰り返さず、C++ コンパイラに強く影響するパフォーマンスの手法のみを説明します。
C++ 関数は、暗黙的に一時オブジェクトを多数生成することがよくあります。これらのオブジェクトは、生成後破棄する必要があります。しかし、そのようなクラスが多数ある場合は、この一時的なオブジェクトの作成と破棄が、処理時間とメモリー使用率という点でかなりの負担になります。C++ コンパイラは一時オブジェクトの一部を削除しますが、すべてを削除できるとはかぎりません。
プログラムの明瞭さを保ちつつ、一時オブジェクトの数が最少になるように関数を記述してください。このための手法としては、暗黙の一時オブジェクトに代わって明示的な変数を使用すること、値パラメータに代わって参照パラメータを使用することなどがあります。また、+ と = だけを実装して使用するのではなく、+= のような演算を実装および使用することもよい手法です。たとえば、次の例の最初の行は、a + b の結果に一時オブジェクトを使用していますが、2 行目は一時オブジェクトを使用していません。
T x = a + b; T x(a); x += b; |
小さくて実行速度の速い関数を呼び出す場合は、通常どおりに呼び出すよりもインライン展開する方が効率が上がります。逆に言えば、大きいか実行速度の遅い関数を呼び出す場合は、分岐するよりもインライン展開する方が効率が悪くなります。また、インライン関数の呼び出しはすべて、関数定義が変更されるたびに再コンパイルする必要があります。このため、インライン関数を使用するかどうかは十分な検討が必要です。
関数定義を変更する可能性があり、呼び出し元をすべて再コンパイルするのに負荷が大きいと予測される場合は、インライン関数は使用しないでください。そうでない場合は、関数をインライン展開するコードが関数を呼び出すコードよりも小さいか、あるいはアプリケーションの動作がインライン関数によって大幅に高速化される場合にのみ使用してください。
コンパイラは、すべての関数呼び出しをインライン展開できるわけではありません。そのため、関数のインライン展開の効率を最高にするにはソースを変更しなければならない場合があります。どのような場合に関数がインライン展開されないかを知るには、+w オプションを使用してください。次のような状況では、コンパイラは関数をインライン展開しません。
ループ、switch 文、try および catch 文のような難しい制御構造が関数に含まれる場合。実際には、これらの関数では、その難しい制御構造はごくまれにしか実行されません。このような関数をインライン展開するには、難しい制御構造が入った内側部分と、内側部分を呼び出すかどうかを決定する外側部分の 2 つに関数を分割します。コンパイラが関数全体をインライン展開できる場合でも、このようによく使用する部分とめったに使用しない部分を分けることで、パフォーマンスを高めることができます。
インライン関数本体のサイズが大きいか、あるいは複雑な場合。見たところ単純な関数本体は、本体内でほかのインライン関数を呼び出していたり、あるいはコンストラクタやデストラクタを暗黙に呼び出していたりするために複雑な場合があります (派生クラスのコンストラクタとデストラクタでこのような状況がよく起きる)。このような関数ではインライン展開でパフォーマンスが大幅に向上することはめったにないため、インライン展開しないことをお勧めします。
インライン関数呼び出しの引数が大きいか、あるいは複雑な場合。インラインメンバー関数を呼び出すためのオブジェクトが、そのインライン関数呼び出しの結果である場合は、パフォーマンスが大幅に下がります。複雑な引数を持つ関数をインライン展開するには、その関数引数を局所変数を使用して関数に渡してください。
クラス定義がパラメータのないコンストラクタ、コピーコンストラクタ、コピー代入演算子、またはデストラクタを宣言しない場合、コンパイラがそれらを暗黙的に宣言します。こうして宣言されたものはデフォルト演算子と呼ばれます。C のような構造体は、デフォルト演算子を持っています。デフォルト演算子は、優れたコードを生成するためにどのような作業が必要かを把握しています。この結果作成されるコードは、ユーザーが作成したコードよりもはるかに高速です。これは、プログラマが通常使用できないアセンブリレベルの機能をコンパイラが利用できるためです。そのため、デフォルト演算子が必要な作業をこなしてくれる場合は、プログラムでこれらの演算子をユーザー定義によって宣言する必要はありません。
デフォルト演算子はインライン関数であるため、インライン関数が適切でない場合にはデフォルト演算子を使用しないでください (前の節を参照)。デフォルト演算子は、次のような場合に適切です。
ユーザーが記述するパラメータのないコンストラクタが、その基底オブジェクトとメンバー変数に対してパラメータのないコンストラクタだけを呼び出す場合。基本の型は、「何も行わない」パラメータのないコンストラクタを効率よく受け入れます。
ユーザーが記述するコピーコンストラクタが、すべての基底オブジェクトとメンバー変数をコピーする場合
ユーザーが記述するコピー代入演算子が、すべての基底オブジェクトとメンバー変数をコピーする場合
ユーザーが記述するデストラクタが空の場合
C++ のプログラミングを紹介する書籍の中には、コードを読んだ際にコードの作成者がデフォルト演算子の効果を考慮に入れていることがわかるように、常にすべての演算子を定義することを勧めているものもあります。しかし、そうすることは明らかに前述した最適化と相入れないものです。デフォルト演算子の使用について明示するには、クラスがデフォルト演算子を使用していることを説明したコメントをコードに入れることをお勧めします。
構造体や共用体などの C++ クラスは、値によって渡され、値によって返されます。POD (Plain-Old-Data) クラスの場合、C++ コンパイラは構造体を C コンパイラと同様に渡す必要があります。クラスを直接渡されたオブジェクト。ユーザー定義のコピーコンストラクタを持つクラスのオブジェクトの場合、コンパイラは実際にオブジェクトのコピーを構築し、コピーにポインタを渡し、ポインタが戻ったあとにコピーを破棄する必要があります。これらのクラスのオブジェクトは、間接的に渡されます。この 2 つの条件の中間に位置するクラスの場合は、コンパイラによってどちらの扱いにするかが選択されます。しかし、そうすることでバイナリ互換性に影響が発生するため、コンパイラは各クラスに矛盾が出ないように選択する必要があります。
ほとんどのコンパイラでは、オブジェクトを直接渡すと実行速度が上がります。特に、複素数や確率値のような小さな値クラスの場合に、実行速度が大幅に上がります。そのためプログラムの効率は、間接的ではなく直接渡される可能性が高いクラスを設計することによって向上する場合があります。
互換モード (-compat[=4]) では、クラスに次の要素が含まれる場合、クラスは間接的に渡されます。
ユーザー定義のコンストラクタ
仮想関数
仮想基底クラス
間接的に渡される基底クラス
間接的に渡される非静的データメンバー
これらの要素が含まれない場合は、クラスは直接渡されます。
標準モード (デフォルトモード) では、クラスに次の要素が含まれる場合、クラスは間接的に渡されます。
ユーザー定義のコピーコンストラクタ
ユーザー定義のデストラクタ
間接的に渡される基底クラス
間接的に渡される非静的データメンバー
これらの要素が含まれない場合は、クラスは直接渡されます。
クラスが直接渡される可能性を最大にするには、次のようにしてください。
可能なかぎりデフォルトのコンストラクタ (特にデフォルトのコピーコンストラクタ) を使用する。
可能なかぎりデフォルトのデストラクタを使用する。デフォルトデストラクタは仮想ではないため、デフォルトデストラクタを使用したクラスは、通常は基底クラスにするべきではありません。
仮想関数と仮想基底クラスを使用しない。
C++ コンパイラによって直接渡されるクラス (および共用体) は、C コンパイラが構造体 (または共用体) を渡す場合とまったく同じように渡されます。しかし、C++ の構造体と共用体の渡し方は、アーキテクチャーによって異なります。
表 10–1 アーキテクチャー別の構造体と共用体の渡し方
アーキテクチャー |
内容の説明 |
---|---|
SPARC V7 および V8 |
構造体と共用体は、呼び出し元で記憶領域を割り当て、その記憶領域へのポインタを渡すことによって渡されます。つまり、構造体と共用体はすべて参照により渡されます。 |
SPARC V9 |
16 バイト (32 バイト) 以下の構造体は、レジスタ中で渡され (返され) ます。共用体およびそのほかのすべての構造体は、呼び出し元で記憶領域を割り当て、その記憶領域へのポインタを渡すことによって渡されます。つまり、小さな構造体はレジスタで渡され、共用体と大きな構造体は参照により渡されます。この結果、小さな値のクラスは基本の型と同じ効率で渡されることになります。 |
x86 プラットフォーム |
構造体と共用体を渡すには、スタックで領域を割り当て、引数をそのスタックにコピーします。構造体と共用体を返すには、呼び出し元のフレームに一時オブジェクトを割り当て、一時オブジェクトのアドレスを暗黙の最初のパラメータとして渡します。 |
C++ メンバー関数では、メンバー変数へのアクセスが頻繁に行われます。
そのため、コンパイラは、this ポインタを介してメモリーからメンバー変数を読み込まなければならないことがよくあります。値はポインタを介して読み込まれているため、次の読み込みをいつ行うべきか、あるいは先に読み込まれている値がまだ有効であるかどうかをコンパイラが決定できないことがあります。このような場合、コンパイラは安全な (しかし遅い) 手法を選択し、アクセスのたびにメンバー変数を再読み込みする必要があります。
不要なメモリー再読み込みが行われないようにするには、次のようにメンバー変数の値を局所変数に明示的にキャッシュしてください。
局所変数を宣言し、メンバー変数の値を使用して初期化する
関数全体で、メンバー変数の代わりに局所変数を使用する
局所変数が変わる場合は、局所変数の最終値をメンバー変数に代入する。しかし、メンバー関数がそのオブジェクトの別のメンバー関数を呼び出す場合には、この最適化のために意図しない結果が発生する場合があります。
この最適化は、基本の型の場合と同様に、値をレジスタに置くことができる場合にもっとも効果的です。また、別名の使用が減ることによりコンパイラの最適化が行われやすくなるため、記憶領域を使用する値にも効果があります。
この最適化は、メンバー変数が明示的に、あるいは暗黙的に頻繁に参照渡しされる場合には逆効果になる場合があります。
現在のオブジェクトとメンバー関数の引数の 1 つの間に別名が存在する可能性がある場合などには、クラスの意味を望ましいものにするために、メンバー変数を明示的にキャッシュしなければならないことがあります。たとえば、次のようにします。
complex& operator*= (complex& left, complex& right) { left.real = left.real * right.real + left.imag * right.imag; left.imag = left.real * right.imag + left.image * right.real; } |
前述のコードが次の指令で呼び出されると、意図しない結果になります。
x*=x; |
この章では、マルチスレッドプログラムの構築方法を説明します。さらに、例外の使用、C++ 標準ライブラリのオブジェクトをスレッド間で共有する方法、従来の (旧形式の) iostream をマルチスレッド環境で使用する方法についても取り上げます。
マルチスレッド処理の詳細については、『マルチスレッドのプログラミングガイド』、『Tools.h++ ユーザガイド』、『標準 C++ ライブラリユーザガイド』を参照してください。
OpenMP 共有メモリー並列化指令を使用してマルチスレッドプログラムを作成する方法の詳細は、『OpenMP API ユーザガイド』も参照してください。
C++ コンパイラに付属しているライブラリは、すべてマルチスレッドで使用しても安全です。マルチスレッドアプリケーションを作成したい場合や、アプリケーションをマルチスレッド化されたライブラリにリンクしたい場合は、-mt オプションを付けてプログラムのコンパイルとリンクを行う必要があります。このオプションを付けると、-D_REENTRANT がプリプロセッサに渡され、-lthread が ld に正しい順番で渡されます。互換性モード (-compat[=4]) の場合、-mt オプションは libthread を libC の前にリンクします。標準モード (デフォルトモード) の場合、-mt オプションは libthread を libCrun の前にリンクします。マクロとライブラリを指定する代わりに、より簡単でエラーの発生しにくい方法である —mt を使用することをお勧めします。
ldd コマンドを使用すると、アプリケーションが libthread にリンクされたかどうかを確認できます。
example% CC -mt myprog.cc example% ldd a.out libm.so.1 => /usr/lib/libm.so.1 libCrun.so.1 => /usr/lib/libCrun.so.1 libthread.so.1 => /usr/lib/libthread.so.1 libc.so.1 => /usr/lib/libc.so.1 libdl.so.1 => /usr/lib/libdl.so.1 |
C++ サポートライブラリ (llibCrun、libiostream、libCstd、libC) は、マルチスレッドで使用しても安全ですが、非同期では安全 (非同期例外で使用しても安全) ではありません。したがって、マルチスレッドアプリケーションのシグナルハンドラでは、これらのライブラリに含まれている関数を使用しないでください。使用するとデッドロックが発生する可能性があります。
マルチスレッドアプリケーションのシグナルハンドラでは、次のものは安全に使用できません。
iostream
new 式と delete 式
例外
現在実装されている例外処理は、マルチスレッドで使用しても安全です。すなわち、あるスレッドの例外によって、別のスレッドの例外が阻害されることはありません。ただし、例外を使用して、スレッド間で情報を受け渡すことはできません。すなわち、あるスレッドから送出された例外を、別のスレッドで捕獲することはできません。
それぞれのスレッドでは、独自の terminate() または unexpected() 関数を設定できます。あるスレッドで呼び出した set_terminate() 関数や set_unexpected() 関数は、そのスレッドの例外だけに影響します。デフォルトの terminate() 関数の内容は、すべてのスレッドで abort() になります。「8.2 実行時エラーの指定」を参照してください。
-noex または -features=no%except、コンパイラオプションが指定されている場合を除き、pthread_cancel(3T) - の呼び出しでスレッドを取り消すと、スタック上の自動オブジェクト (静的ではない局所オブジェクト) が破棄されます。
pthread_cancel(3T) では、例外と同じ仕組みが使用されます。スレッドが取り消されると、局所デストラクタの実行中に、ユーザーが pthread_cleanup_push() を使用して登録したクリーンアップルーチンが実行されます。クリーンアップルーチンの登録後に呼び出した関数の局所オブジェクトは、そのクリーンアップルーチンが実行される前に破棄されます。
C++ 標準ライブラリ (libCstd -library=Cstd) は、いくつかのロケールを除けばマルチスレッドで使用しても安全なライブラリで、このライブラリの内部は、マルチスレッド環境で正しく機能することが保証されています。ただし、依然、スレッド間で共有するライブラリオブジェクト周りには注意を払う必要があります。setlocale(3C) および attributes(5) のマニュアルページを参照してください。
たとえば、文字列をインスタンス化し、この文字列を新しく生成したスレッドに参照で渡した場合を考えてみましょう。この文字列への書き込みアクセスはロックする必要があります。なぜなら、同じ文字列オブジェクトを、プログラムが複数のスレッドで明示的に共有しているからです。この処理を行うために用意されたライブラリの機能については後述します。
これに対して、この文字列を新しいスレッドに値で渡した場合は、ロックについて考慮する必要はありません。このことは、Rogue Wave の「書き込み時コピー」機能により、2 つのスレッドの別々の文字列が同じ表現を共有している場合にも当てはまります。このような場合のロックは、ライブラリが自動的に処理します。プログラム自身でロックを行う必要があるのは、スレッド間での参照渡しや、大域オブジェクトや静的オブジェクトを使用して、同じオブジェクトを複数のスレッドから明示的に使用できるようにした場合だけです。
ここからは、複数のスレッドが存在する場合の動作を保証するために、C++ 標準ライブラリの内部で使用されるロック (同期) 機能について説明します。
マルチスレッドでの安全性を実現する機能は、2 つの同期クラス、_RWSTDMutex と _RWSTDGuard によって提供されます。
_RWSTDMutex クラスは、プラットフォームに依存しないロック機能を提供します。このクラスには、次のメンバー関数があります。
void acquire()— 自分自身に対するロックを獲得する。または、このロックを獲得できるまでブロックする。
void release()— 自分自身に対するロックを解除する。
class _RWSTDMutex { public: _RWSTDMutex (); ~_RWSTDMutex (); void acquire (); void release (); }; |
_RWSTDGuard クラスは、_RWSTDMutex クラスのオブジェクトをカプセル化するための便利なラッパークラスです。_RWSTDGuard クラスのオブジェクトは、自分自身のコンストラクタの中で、カプセル化された相互排他ロック (mutex) を獲得しようとします。エラーが発生した場合は、このコンストラクタは std::exception から派生している ::thread_error 型の例外を送出します。獲得された相互排他ロックは、このオブジェクトのデストラクタの中で解除されます。このデストラクタは例外を送出しません。
class _RWSTDGuard { public: _RWSTDGuard (_RWSTDMutex&); ~_RWSTDGuard (); }; |
さらに、_RWSTD_MT_GUARD(mutex) マクロ (従来の _STDGUARD) を使用すると、マルチスレッドの構築時にだけ _RWSTDGuard クラスのオブジェクトを生成できます。生成されたオブジェクトは、そのオブジェクトが定義されたコードブロックの残りの部分が、複数のスレッドで同時に実行されないようにします。単一スレッドの構築時には、このマクロは空白の式に展開されます。
これらの機能は、次のように使用します。
#include <rw/stdmutex.h> // // An integer shared among multiple threads. // int I; // // A mutex used to synchronize updates to I. // _RWSTDMutex I_mutex; // // Increment I by one. Uses an _RWSTDMutex directly. // void increment_I () { I_mutex.acquire(); // Lock the mutex. I++; I_mutex.release(); // Unlock the mutex. } // // Decrement I by one. Uses an _RWSTDGuard. // void decrement_I () { _RWSTDGuard guard(I_mutex); // Acquire the lock on I_mutex. --I; // // The lock on I is released when destructor is called on guard. // } |
この節では、libC ライブラリと libiostream ライブラリの iostream クラスを、マルチスレッド環境での入出力に使用する方法を説明します。さらに、iostream クラスの派生クラスを作成し、ライブラリの機能を拡張する例も紹介します。ここでは、C++ のマルチスレッドコードを記述するための指針は示しません。
この節では、従来の iostream (libC と libiostream) だけを取り扱います。この節の説明は、C++ 標準ライブラリに含まれている新しい iostream (libCstd) には当てはまりません。
iostream ライブラリのインタフェースは、マルチスレッド環境用のアプリケーション、すなわちサポートされている Solaris オペレーティングシステムのバージョンで実行される、マルチスレッド機能を使用するプログラムから使用できます。従来のライブラリのシングルスレッド機能を使用するアプリケーションは影響を受けません。
ライブラリが「マルチスレッドを使用しても安全」といえるのは、複数のスレッドが存在する環境で正しく機能する場合です。一般に、ここでの「正しく機能する」とは、公開関数がすべて再入可能なことを指します。iostream ライブラリには、複数のスレッドの間で共有されるオブジェクト (C++ クラスのインスタンス) の状態が、複数のスレッドから変更されるのを防ぐ機能があります。ただし、iostream オブジェクトがマルチスレッドで使用しても安全になるのは、そのオブジェクトの公開メンバー関数が実行されている間に限られます。
アプリケーションで libC ライブラリからマルチスレッドで使用しても安全なオブジェクトを使用しているからといって、そのアプリケーションが自動的にマルチスレッドで使用しても安全になるわけではありません。アプリケーションがマルチスレッドで使用しても安全になるのは、マルチスレッド環境で想定したとおりに実行される場合だけです。
マルチスレッドで使用しても安全な iostream ライブラリの構成は、従来の iostream ライブラリの構成と多少異なります。マルチスレッドで使用しても安全な iostream ライブラリのインタフェースは、iostream クラスやその基底クラスの公開および限定公開のメンバー関数を示していて、従来のライブラリと整合性が保たれていますが、クラス階層に違いがあります。詳細については、「11.4.2 iostream ライブラリのインタフェースの変更」を参照してください。
従来の中核クラスの名前が変更されています (先頭に unsafe_ という文字列が付きました)。iostream パッケージの中核クラスを表 11–1 に示します。
表 11–1 iostream の中核クラス
クラス |
内容の説明 |
---|---|
stream_MT |
マルチスレッドで使用しても安全なクラスの基底クラス |
streambuf |
バッファーの基底クラス |
unsafe_ios |
各種のストリームクラスに共通の状態変数 (エラー状態、書式状態など) を収容するクラス |
unsafe_istream |
streambuf から取り出した文字の並びを、書式付き/書式なし変換する機能を持つクラス |
unsafe_ostream |
streambuf に格納する文字の並びを、書式付き/書式なし変換する機能を持つクラス |
unsafe_iostream |
unsafe_istream クラスと unsafe_ostream クラスを組み合わせた入出力兼用のクラス |
マルチスレッドで使用しても安全なクラスは、すべて基底クラス stream_MT の派生クラスです。また、これらのクラスは、streambuf を除いて、(先頭に unsafe_ が付いた) 従来の基底クラスの派生クラスでもあります。この例を次に示します。
class streambuf: public stream_MT {...}; class ios: virtual public unsafe_ios, public stream_MT {...}; class istream: virtual public ios, public unsafe_istream {...}; |
stream_MT には、それぞれの iostream クラスをマルチスレッドで使用しても安全にするための相互排他 (mutex) ロック機能が含まれています。また、このクラスには、マルチスレッドで使用しても安全な属性を動的に変更できるように、ロックを動的に有効および無効にする機能もあります。入出力変換とバッファー管理の基本機能は、従来の unsafe_ クラスにまとめられています。したがって、ライブラリに新しく追加されたマルチスレッドで使用しても安全な機能は、その派生クラスだけで使用できます。マルチスレッドで使用しても安全なクラスには、従来の unsafe_ 基底クラスと同じ公開メンバー関数と限定公開メンバー関数が含まれています。これらのメンバー関数は、オブジェクトをロックし、unsafe_ 基底クラスの同名の関数を呼び出し、そのあとでオブジェクトのロックを解除するラッパーとして働きます。
streambuf クラスは、unsafe クラスの派生クラスではありません。streambuf クラスの公開メンバー関数と限定公開メンバー関数は、ロックを行うことで再入可能になります。ロックを行わない関数も用意されています。これらの関数は、名前の後ろに _unlocked という文字列が付きます。
iostream のインタフェースには、マルチスレッドで使用しても安全な、再入可能な公開関数が追加されています。これらの関数は、追加引数としてユーザーが指定したバッファーを受け取ります。これらの関数を次に示します。
表 11–2 マルチスレッドで使用しても安全な、再入可能な公開関数
関数 |
内容の説明 |
---|---|
char *oct_r (char *buf, int buflen, long num, int width) |
数値を 8 進数の形式で表現した ASCII 文字列のポインタを返す。width が 0 (ゼロ) ではない場合は、その値が書式設定用のフィールド幅になります。戻り値は、ユーザーが用意したバッファーの先頭を指すとはかぎりません。 |
char *hex_r (char *buf, int buflen, long num, int width) |
数値を 16 進数の形式で表現した ASCII 文字列のポインタを返す。width が 0 (ゼロ) ではない場合は、その値が書式設定用のフィールド幅になります。戻り値は、ユーザーが用意したバッファーの先頭を指すとはかぎりません。 |
char *dec_r (char *buf, int buflen, long num, int width) |
数値を 10 進数の形式で表現した ASCII 文字列のポインタを返す。width が 0 (ゼロ) ではない場合は、その値が書式設定用のフィールド幅になります。戻り値は、ユーザーが用意したバッファーの先頭を指すとはかぎりません。 |
char *chr_r (char *buf, int buflen, long num, int width) |
文字 chr を含む ASCII 文字列のポインタを返す。width が 0 (ゼロ) ではない場合は、その値と同じ数の空白に続けて chr が格納されます。戻り値は、ユーザーが用意したバッファーの先頭を指すとはかぎりません。 |
char *form_r (char *buf, int buflen, long num, int width) |
sprintf によって書式設定した文字列のポインタを返す。書式文字列 format 以降のすべての引数を使用します。ユーザーが用意したバッファーに、変換後の文字列を収容できるだけの大きさがなければいけません。 |
以前の libC との互換性を確保するために提供されている iostream ライブラリの公開変換ルーチン (oct、hex、dec、chr、form) は、マルチスレッドで使用すると安全ではありません。
libC ライブラリの iostream クラスを使用した、マルチスレッド環境用のアプリケーションを構築するには、-mt オプションを付けてソースコードのコンパイルとリンクを行う必要があります。このオプションを付けると、プリプロセッサに -D_REENTRANT が渡され、リンカーに -lthread が渡されます。
libC と libthread へのリンクを行うには、(-lthread オプションではなく) -mt オプションを使用します。このオプションを使用しないと、ライブラリが正しい順番でリンクされないことがあります。誤って -lthread オプションを使用すると、作成したアプリケーションが正しく機能しない場合があります。
iostream クラスを使用するシングルスレッドアプリケーションについては、コンパイラオプションやリンカーオプションを特に必要としません。オプションを何も指定しなかった場合は、コンパイラは libC ライブラリへのリンクを行います。
iostream ライブラリのマルチスレッドでの安全性には制約があります。これは、マルチスレッド環境で iostream オブジェクトが共有された場合に、iostream を使用するプログラミング手法の多くが安全ではなくなるためです。
マルチスレッドでの安全性を実現するには、エラーの原因になる入出力操作を含んでいる危険領域で、エラーチェックを行う必要があります。エラーが発生したかどうかを確認するには次のようにします。
#include <iostream.h> enum iostate {IOok, IOeof, IOfail}; iostate read_number(istream& istr, int& num) { stream_locker sl(istr, stream_locker::lock_now); istr >> num; if (istr.eof()) return IOeof; if (istr.fail()) return IOfail; return IOok; } |
この例では、stream_locker オブジェクト sl のコンストラクタによって、istream オブジェクト istr がロックされます。このロックは、read_number が終了したときに呼び出される sl のデストラクタによって解除されます istr。
マルチスレッドでの安全性を実現するには、最後の入力操作と gcount の呼び出しを行う期間に、istream オブジェクトを排他的に使用するスレッドの内部から、gcount 関数を呼び出す必要があります。gcount は次のように呼び出します。
#include <iostream.h> #include <rlocks.h> void fetch_line(istream& istr, char* line, int& linecount) { stream_locker sl(istr, stream_locker::lock_defer); sl.lock(); // lock the stream istr istr >> line; linecount = istr.gcount(); sl.unlock(); // unlock istr ... } |
この例では、stream_locker クラスの lock メンバー関数を呼び出してから unlock メンバー関数を呼び出すまでが、プログラムの相互排他領域になります。
マルチスレッドでの安全性を実現するには、別々の操作を特定の順番で行う必要があるユーザー定義型用の入出力操作を、危険領域としてロックする必要があります。この入出力操作の例を次に示します。
#include <rlocks.h> #include <iostream.h> class mystream: public istream { // other definitions... int getRecord(char* name, int& id, float& gpa); }; int mystream::getRecord(char* name, int& id, float& gpa) { stream_locker sl(this, stream_locker::lock_now); *this >> name; *this >> id; *this >> gpa; return this->fail() == 0; } |
現行の libC ライブラリに含まれているマルチスレッドで使用しても安全なクラスを使用すると、シングルスレッドアプリケーションの場合でさえも多少のオーバーヘッドが発生します。libC の unsafe_ クラスを使用すると、このオーバーヘッドを回避できます。
次のようにスコープ決定演算子を使用すると、unsafe_ 基底クラスのメンバー関数を実行できます。
cout.unsafe_ostream::put(’4’); |
cin.unsafe_istream::read(buf, len); |
unsafe_ クラスは、マルチスレッドアプリケーションでは安全に使用できません。
unsafe_ クラスを使用する代わりに、cout オブジェクトと cin オブジェクトを unsafe にしてから、通常の操作を行うこともできます。ただし、パフォーマンスが若干低下します。unsafe な cout と cin は、次のように使用します。
#include <iostream.h> //disable mt-safety cout.set_safe_flag(stream_MT::unsafe_object); //disable mt-safety cin.set_safe_flag(stream_MT::unsafe_object); cout.put(”4’); cin.read(buf, len); |
iostream オブジェクトがマルチスレッドで使用しても安全な場合は、相互排他ロックを行うことで、そのオブジェクトのメンバー変数が保護されます。 しかし、シングルスレッド環境でしか実行されないアプリケーションでは、このロック処理のために、本来なら必要のないオーバーヘッドがかかります。iostream オブジェクトのマルチスレッドでの安全性の有効/無効を動的に切り替えると、パフォーマンスを改善できます。たとえば、iostream オブジェクトのマルチスレッドでの安全性を無効にするには、次のようにします。
fs.set_safe_flag(stream_MT::unsafe_object);// disable MT-safety .... do various i/o operations |
iostream が複数のスレッド間で共有されないコード領域では、マルチスレッドでの安全性の無効化ストリームであっても、安全に使用できます。たとえば、スレッドが 1 つしかないプログラムや、スレッドごとに非公開の iostream を使用するプログラムでは問題は起きません。
プログラムに同期処理を明示的に挿入すると、iostream が複数のスレッド間で共有される場合にも、マルチスレッドで使用すると安全ではない iostream を安全に使用できるようになります。この例を次に示します。
generic_lock(); fs.set_safe_flag(stream_MT::unsafe_object); ... do various i/o operations generic_unlock(); |
ここで、generic_lock 関数と generic_unlock 関数は、相互排他ロック (mutex)、セマフォー、読み取り/書き込みロックといった基本型を使用する同期機能であれば、何でもかまいません。
libC クラスによって提供される stream_locker クラスは、この目的で優先される機構です。
詳細は 「11.4.5 オブジェクトのロック」を参照してください。
この節では、iostream ライブラリをマルチスレッドで使用しても安全にするために行われたインタフェースの変更内容について説明します。
libC インタフェースに追加された新しいクラスを次の表に示します。
stream_MT stream_locker unsafe_ios unsafe_istream unsafe_ostream unsafe_iostream unsafe_fstreambase unsafe_strstreambase |
iostream インタフェースに追加された新しいクラス階層を次の表に示します。
class streambuf: public stream_MT {...}; class unsafe_ios {...}; class ios: virtual public unsafe_ios, public stream_MT {...}; class unsafe_fstreambase: virtual public unsafe_ios {...}; class fstreambase: virtual public ios, public unsafe_fstreambase {...}; class unsafe_strstreambase: virtual public unsafe_ios {...}; class strstreambase: virtual public ios, public unsafe_strstreambase {...}; class unsafe_istream: virtual public unsafe_ios {...}; class unsafe_ostream: virtual public unsafe_ios {...}; class istream: virtual public ios, public unsafe_istream {...}; class ostream: virtual public ios, public unsafe_ostream {...}; class unsafe_iostream: public unsafe_istream, public unsafe_ostream {...}; |
iostream インタフェースに追加された新しい関数を次の表に示します。
class streambuf { public: int sgetc_unlocked(); void sgetn_unlocked(char *, int); int snextc_unlocked(); int sbumpc_unlocked(); void stossc_unlocked(); int in_avail_unlocked(); int sputbackc_unlocked(char); int sputc_unlocked(int); int sputn_unlocked(const char *, int); int out_waiting_unlocked(); protected: char* base_unlocked(); char* ebuf_unlocked(); int blen_unlocked(); char* pbase_unlocked(); char* eback_unlocked(); char* gptr_unlocked(); char* egptr_unlocked(); char* pptr_unlocked(); void setp_unlocked(char*, char*); void setg_unlocked(char*, char*, char*); void pbump_unlocked(int); void gbump_unlocked(int); void setb_unlocked(char*, char*, int); int unbuffered_unlocked(); char *epptr_unlocked(); void unbuffered_unlocked(int); int allocate_unlocked(int); }; class filebuf: public streambuf { public: int is_open_unlocked(); filebuf* close_unlocked(); filebuf* open_unlocked(const char*, int, int = filebuf::openprot); filebuf* attach_unlocked(int); }; class strstreambuf: public streambuf { public: int freeze_unlocked(); char* str_unlocked(); }; unsafe_ostream& endl(unsafe_ostream&); unsafe_ostream& ends(unsafe_ostream&); unsafe_ostream& flush(unsafe_ostream&); unsafe_istream& ws(unsafe_istream&); unsafe_ios& dec(unsafe_ios&); unsafe_ios& hex(unsafe_ios&); unsafe_ios& oct(unsafe_ios&); char* dec_r (char* buf, int buflen, long num, int width) char* hex_r (char* buf, int buflen, long num, int width) char* oct_r (char* buf, int buflen, long num, int width) char* chr_r (char* buf, int buflen, long chr, int width) char* str_r (char* buf, int buflen, const char* format, int width = 0); char* form_r (char* buf, int buflen, const char* format,...) |
マルチスレッドアプリケーションでの大域データと静的データは、スレッド間で安全に共有されません。スレッドはそれぞれ個別に実行されますが、同じプロセス内のスレッドは、大域オブジェクトと静的オブジェクトへのアクセスを共有します。このような共有オブジェクトをあるスレッドで変更すると、その変更が同じプロセス内のほかのスレッドにも反映されるため、状態を保つことが難しくなります。C++ では、クラスオブジェクト (クラスのインスタンス) の状態は、メンバー変数の値が変わると変化します。そのため、共有されたクラスオブジェクトは、ほかのスレッドからの変更に対して脆弱です。
マルチスレッドアプリケーションで iostream ライブラリを使用し、iostream.h をインクルードすると、デフォルトでは標準ストリーム (cout、cin、cerr、clog) が大域的な共有オブジェクトとして定義されます。iostream ライブラリはマルチスレッドで使用しても安全なので、iostream オブジェクトのメンバー関数の実行中は、共有オブジェクトの状態が、ほかのスレッドからのアクセスや変更から保護されます。ただし、オブジェクトがマルチスレッドで使用しても安全なのは、そのオブジェクトの公開メンバー関数が実行されている間だけです。たとえば、次を見てください。
int c; cin.get(c); |
このコードを使用して、スレッド A が get バッファーの次の文字を取り出し、バッファーポインタを更新したとします。ところが、スレッド A が、次の命令で再び get を呼び出したとしても、libC ライブラリはシーケンスのその次の文字を返すことを保証しません。なぜなら、スレッド A の 2 つの get の呼び出しの間に、スレッド B からも別の get が呼び出される可能性があるからです。
このような共有オブジェクトとマルチスレッド処理の問題に対処する方法については、「11.4.5 オブジェクトのロック」を参照してください。
iostream オブジェクトを使用した場合に、一続きの入出力操作をマルチスレッドで使用しても安全にしなければならない場合がよくあります。次は
cout << " Error message:" << errstring[err_number] << "\n"; |
このコードでは、cout ストリームオブジェクトの 3 つのメンバー関数が実行されます。cout は共有オブジェクトなので、マルチスレッド環境では、この操作全体を危険領域として不可分的に (連続して) 実行しなければなりません。iostream クラスのオブジェクトに対する一続きの操作を不可分的に実行するには、何らかのロック処理が必要です。
iostream オブジェクトをロックできるように、libC ライブラリに新しく stream_locker クラスが追加されています。stream_locker クラスの詳細については、「11.4.5 オブジェクトのロック」を参照してください。
共有オブジェクトに対処する方法とマルチスレッド化は iostream オブジェクトをスレッドの局所的なオブジェクトにして、問題そのものを解消してしまうことです。たとえば、次を見てください。
スレッドのエントリ関数の中でオブジェクトを局所的に宣言する。
スレッド固有データの中でオブジェクトを宣言する。(スレッド固有データの使用法については、thr_keycreate(3T) のマニュアルページを参照してください。
ストリームオブジェクトを特定のスレッド専用にする。このオブジェクトスレッドは、慣例により非公開 (private) になります。
ただし、デフォルトの共有標準ストリームオブジェクトを初めとして、多くの場合はオブジェクトをスレッドの局所的なオブジェクトにすることはできません。そのため、別の手段が必要です。
iostream クラスのオブジェクトに対する一続きの操作を不可分的に実行するには、何らかのロック処理が必要です。ただし、ロック処理を行うと、シングルスレッドアプリケーションの場合でさえも、オーバーヘッドが多少増加します。ロック処理を追加する必要があるか、それとも iostream オブジェクトをスレッドの非公開オブジェクトにすればよいかは、アプリケーションで採用しているスレッドモデル (独立スレッドと連携スレッドのどちらを使用しているか) によって決まります。
スレッドごとに別々の iostream オブジェクトを使用してデータを入出力する場合は、それぞれの iostream オブジェクトが、該当するスレッドの非公開オブジェクトになります。ロック処理の必要はありません。
複数のスレッドを連携させる (これらのスレッドの間で、同じ iostream オブジェクトを共有させる) 場合は、その共有オブジェクトへのアクセスの同期をとる必要があり、何らかのロック処理によって、一続きの操作を不可分的にする必要があります。
iostream ライブラリには、iostream オブジェクトに対する一続きの操作をロックするための stream_locker クラスが含まれています。これにより、iostream オブジェクトのロックを動的に切り換えることで生じるオーバーヘッドを最小限にできます。
stream_locker クラスのオブジェクトを使用すると、ストリームオブジェクトに対する一続きの操作を不可分的にできます。たとえば、次の例を考えてみましょう。このコードは、ファイル内の位置を特定の場所まで移動し、その後続のデータブロックを読み込みます。
#include <fstream.h> #include <rlocks.h> void lock_example (fstream& fs) { const int len = 128; char buf[len]; int offset = 48; stream_locker s_lock(fs, stream_locker::lock_now); .....// open file fs.seekg(offset, ios::beg); fs.read(buf, len); } |
この例では、stream_locker オブジェクトのコンストラクタが実行されてから、デストラクタが実行されるまでが、一度に 1 つのスレッドしか実行できない相互排他領域になります。デストラクタは、lock_example 関数が終了したときに呼び出されます。この stream_locker オブジェクトにより、ファイル内の特定のオフセットへの移動と、ファイルからの読み込みの連続的な (不可分的な) 実行が保証され、ファイルからの読み込みを行う前に、別のスレッドによってオフセットが変更されてしまう可能性がなくなります。
stream_locker オブジェクトを使用して、相互排他領域を明示的に定義することもできます。次の例では、入出力操作と、そのあとで行うエラーチェックを不可分的にするために、stream_locker オブジェクトのメンバー関数、lock と unlock を呼び出しています。
{ ... stream_locker file_lck(openfile_stream, stream_locker::lock_defer); .... file_lck.lock(); // lock openfile_stream openfile_stream << "Value: " << int_value << "\n"; if(!openfile_stream) { file_error("Output of value failed\n"); return; } file_lck.unlock(); // unlock openfile_stream } |
詳細は、stream_locker(3CC4) のマニュアルページを参照してください。
iostream クラスから新しいクラスを派生させて、機能を拡張または特殊化できます。マルチスレッド環境で、これらの派生クラスからインスタンス化したオブジェクトを使用する場合は、その派生クラスがマルチスレッドで使用しても安全でなければなりません。
マルチスレッドで使用しても安全なクラスを派生させる場合は、次のことに注意する必要があります。
クラスオブジェクトの内部状態を複数のスレッドによる変更から保護し、そのオブジェクトをマルチスレッドで使用しても安全にします。そのためには、公開および限定公開のメンバー関数に含まれているメンバー変数へのアクセスを、相互排他ロックで直列化します。
マルチスレッドで使用しても安全な基底クラスのメンバー関数を、一続きに呼び出す必要がある場合は、それらの呼び出しを stream_locker オブジェクトを使用して不可分にします。
stream_locker オブジェクトで定義した危険領域の内部では、streambuf クラスの _unlocked メンバー関数を使用して、ロック処理のオーバーヘッドを防止します。
streambuf クラスの公開仮想関数を、アプリケーションから直接呼び出す場合は、それらの関数をロックします。該当する関数は、次のとおりです: xsgetn、underflow、pbackfail、xsputn、overflow、seekoff、seekpos。
ios クラスの iword メンバー関数と pword メンバー関数を使用して、ios オブジェクトの書式設定状態を拡張します。ただし、複数のスレッドが iword 関数や pword 関数の同じ添字を共有している場合は、問題が発生することがあります。これらのスレッドをマルチスレッドで使用しても安全にするには、適切なロック機能を使用する必要があります。
メンバー関数のうち、char 型よりも大きなサイズのメンバー変数値を返すものをロックします。
複数のスレッドの間で共有される iostream オブジェクトを削除するには、サブスレッドがそのオブジェクトの使用を終えていることを、メインスレッドで確認する必要があります。共有オブジェクトを安全に破棄する方法を次に示します。
#include <fstream.h> #include <thread.h> fstream* fp; void *process_rtn(void*) { // body of sub-threads which uses fp... } void multi_process(const char* filename, int numthreads) { fp = new fstream(filename, ios::in); // create fstream object // before creating threads. // create threads for (int i=0; i<numthreads; i++) thr_create(0, STACKSIZE, process_rtn, 0, 0, 0); ... // wait for threads to finish for (int i=0; i<numthreads; i++) thr_join(0, 0, 0); delete fp; // delete fstream object after fp = NULL; // all threads have completed. } |
ここでは、libC ライブラリの iostream オブジェクトを安全な方法で使用するマルチスレッドアプリケーションの例を示します。
このアプリケーションは、最大で 255 のスレッドを生成します。それぞれのスレッドは、別々の入力ファイルを 1 行ずつ読み込み、標準出力ストリーム cout を介して共通の出力ファイルに書き出します。この出力ファイルは、すべてのスレッドから共有されるため、出力操作がどのスレッドから行われたかを示す値をタグとして付けます。
// create tagged thread data // the output file is of the form: // <tag><string of data>\n // where tag is an integer value in a unsigned char. // Allows up to 255 threads to be run in this application // <string of data> is any printable characters // Because tag is an integer value written as char, // you need to use od to look at the output file, suggest: // od -c out.file |more #include <stdlib.h> #include <stdio.h> #include <iostream.h> #include <fstream.h> #include <thread.h> struct thread_args { char* filename; int thread_tag; }; const int thread_bufsize = 256; // entry routine for each thread void* ThreadDuties(void* v) { // obtain arguments for this thread thread_args* tt = (thread_args*)v; char ibuf[thread_bufsize]; // open thread input file ifstream instr(tt->filename); stream_locker lockout(cout, stream_locker::lock_defer); while(1) { // read a line at a time instr.getline(ibuf, thread_bufsize - 1, ’\n’); if(instr.eof()) break; // lock cout stream so the i/o operation is atomic lockout.lock(); // tag line and send to cout cout << (unsigned char)tt->thread_tag << ibuf << "\n"; lockout.unlock(); } return 0; } int main(int argc, char** argv) { // argv: 1+ list of filenames per thread if(argc < 2) { cout << “usage: " << argv[0] << " <files..>\n"; exit(1); } int num_threads = argc - 1; int total_tags = 0; // array of thread_ids thread_t created_threads[thread_bufsize]; // array of arguments to thread entry routine thread_args thr_args[thread_bufsize]; int i; for(i = 0; i < num_threads; i++) { thr_args[i].filename = argv[1 + i]; // assign a tag to a thread - a value less than 256 thr_args[i].thread_tag = total_tags++; // create threads thr_create(0, 0, ThreadDuties, &thr_args[i], THR_SUSPENDED, &created_threads[i]); } for(i = 0; i < num_threads; i++) { thr_continue(created_threads[i]); } for(i = 0; i < num_threads; i++) { thr_join(created_threads[i], 0, 0); } return 0; } |