[Contents]   [Back]   [Prev]   [Up]   [Next]   [Forward]  


C++言語に対する拡張機能

GNUコンパイラは、 C++言語に対して、 以下に示す拡張機能を提供しています (さらに、 C言語に対する拡張機能のほとんども、 C++プログラムの中で使うことができます)。 これらの拡張機能が利用可能であるかどうかをチェックするソース・コードを書きたいのであれば、 Cのプログラムの場合と同様の方法によって、 GNUコンパイラであるかどうかをテストすることができます。 それは、 あらかじめ定義されたマクロ__GNUC__があるかどうかをチェックするという方法です。 特にGNU C++であるかどうかをテストするには、 __GNUG__を使うことができます。 (The C Preprocessorの`Standard Predefined Macros'を参照)

C++における名前付き戻り値

GNU C++では、 関数定義の構文を拡張することによって、 C++プログラムの関数の戻り値の名前を、 定義本体の外で指定することができます。

type
functionname (args) return resultname;
{
  ...
  body
  ...
}

ある関数がクラス型の戻り値を持つ場合に、 生成関数が余分に呼び出されるのを回避するために、 この機能を使うことができます。 例えば、 `X v = m ();'として宣言される、 クラスX型の戻り値を持つ関数mを考えてみましょう。

X
m ()
{
  X b;
  b.a = 23;
  return b;
}

mは引数を1つも取らないように見えますが、 実際には暗黙の引数を1つ取ります。 それは戻り値のアドレスです。 関数の呼び出し時に、 vを保持するのに十分な大きさの領域へのアドレスが、 暗黙の引数として渡されます。 そのあとでbが生成され、 そのフィールドaに23という値がセットされます。 最後に、 コピー生成関数 (`X(X&)'という形式の生成関数) がbに対して適用される際に、 (暗黙のうちに渡された) 戻り値の格納位置がその結果の格納先として使われ、 こうしてvが戻り値に結合されます。

しかし、 ここには無駄があります。 局所変数bは、 ただちに外部にコピーされてしまうものを保持するためだけの目的で宣言されています。 プロシージャ間(interprocedural)データ・フロー解析に 「省略(elision)」アルゴリズムを組み合わせたコンパイラであれば、 おそらくはこのような無駄のすべてを除去することができるでしょう。 しかしそれよりも、 明示的に戻り値を操作することによって、 コンパイラが効率的なコードを生成するのを支援することで、 結果として局所変数やコピー生成関数をすべて回避できるようにする方がはるかに実用的でしょう。

GNU C++の拡張された関数定義構文を使うことで、 最初に戻り値にrという名前を与え、 そのフィールドaに直接代入することによって、 一時的な割り当てやコピーの作成を回避することができます。

X
m () return r;
{
  r.a = 23;
}

rの宣言は標準的かつ正当な宣言であり、 mの本体のどの部分が実行されるよりも前に、 その宣言が実行されます。

このような種類の関数を使うことで、 追加的な制限が加えられることはありません。 return文を実行することもできますし、 関数本体の終端に達する (「端から落ちる」) ことによって暗黙のうちに復帰することもできます。

X
m () return r (23);
{
  return;
}

のような場合でも (あるいは、 `X m () return r (23); { }'のような場合ですら)、 その意味は不明瞭ではありません。 というのは、 いずれの場合でも戻り値rが初期化済みであるからです。 以下のソース・コードは読みにくいかもしれませんが、 その動作を予測することはできます。

X
m () return r;
{
  X b;
  return b;
}

rによって表わされる戻り値の格納先が最初に初期化されますが、 `return b;'という文によって、 その値は無効になります。 コンパイラは、 rを破棄して (この際、 消去関数があればそれを呼び出しますが、 消去関数がなければ何も実行しません)、 そのあとでrbによって再初期化することによって、 これを処理します。

この拡張機能は主に、 多重定義された(overloaded)演算子を使う人々を支援するために提供されています。 多重定義された演算子を使うと、 関数の引数だけではなく戻り値も制御する必要が大いにあります。 コピー生成関数が性能に対して重大な悪影響をもたらすようなクラスについては (特に、 高速なデフォルトの生成関数が存在する一般的なケースでは)、 これは大きな節約になります。 この拡張機能の不利な点は、 戻り値のデフォルトの生成関数がいつ呼び出されるかを制御できないということです。 それは、 常に最初に呼び出されます。

C++における最小値演算子と最大値演算子

2つの引数の「最小値」または「最大値」を返してくれる演算子があると非常に便利です。 GNU C++では、 以下の演算子が利用できます (GNU Cでは利用できません)。

a <? b
数値abのうち小さいほうを返す最小値演算子
a >? b
数値abのうち大きいほうを返す最大値演算子

これらの演算子は、 通常のC++においてはプリミティブ(primitive)ではありません。 以下の例に示すように、 C++においては、 2つの値の最小値を返すマクロを使うことができるからです。

#define MIN(X,Y) ((X) < (Y) ? : (X) : (Y))

変数ijの最小値をminにセットするには、 `int min = MIN (i, j);'のように使います。

しかし、 XまたはYに副作用があると、 意図されない振る舞いをすることになるかもしれません。 例えば、 MIN (i++, j++)は、 小さいほうのカウンタ値を2回インクリメントすることになるので、 正しく動作しません。 GNU Cの拡張機能によって、 このような問題を回避できる安全なマクロを書くことができます (式の型に対する名前付けを参照)。 しかし、 MINMAXをマクロとして書くと、 基本的な算術演算に対して関数呼び出しの表記法を使うことを強制されることにもなります。 GNU C++の拡張機能を使えば、 `int min = i <? j;'のように書くことができます。

<?>?はコンパイラの内部に組み込まれているので、 副作用を持つ式も正しく処理することができます。 `int min = i++ <? j++;'は正しく動作します。

GNU C++におけるgotoと消去関数

C++のプログラムでは、 goto文を使っても安全です。 消去関数を必要とする集合体を包含しているブロックの中から外に出るのにgoto文を使うと、 gotoが制御を移す前にその消去関数が実行されます。

しかし、 生成関数を必要とするスコープの内部に入るためにgotoを使うことは、 コンパイラが許しません。

1つのヘッダ・ファイルにおける宣言と定義

C++のオブジェクト定義はかなり複雑になることがあります。 原則として、 複数のソース・ファイルにわたって使う個々のオブジェクトについて、 ソース・コードの中で必要なものが2つあります。 第1に、 型宣言と関数プロトタイプによってオブジェクトの構造を記述するインターフェイス仕様が必要です。 第2に、 オブジェクトの実装そのものが必要です。 ヘッダ・ファイル中に分離されたインターフェイスの記述を、 実際の実装の記述に正しく対応するよう保守するのは、 退屈な作業になることがあります。 インターフェイス定義と実装定義が分離されると、 両者は正しく対応しなくなるかもしれないので、 これは危険でもあります。

GNU C++では、 ただ1つのヘッダ・ファイルを使ってこれら2つの目的を実現することができます。

警告: これを指定するメカニズムは、 現在変更される過渡期にあります。 当面は、 以下に説明する2つの#pragmaコマンドのどちらかを使わなければなりません。 将来のGNU C++のリリースでは、 これに代わるメカニズムによって、 これらの#pragmaコマンドは不必要になるでしょう。

まず、 ヘッダ・ファイルの中に完全なオブジェクト定義を書きます。 ただし、 そのヘッダ・ファイルのソース・コードに`#pragma interface'というマークを付けます。 これによってコンパイラは、 普通のソース・ファイルが#includeによってそのヘッダ・ファイルを組み込む場合には、 インターフェイス仕様としての用途においてのみ、 そのヘッダ・ファイルを使うことができるようになります。 完全な実装定義が組み込まれるべき唯一のソース・ファイルの中では、 ヘッダ・ファイルの持っている完全な実装定義を与えるという異なる用途を利用することを示すために、 名前付け規約と`#pragma implementation'のどちらかを使うことができます。

#pragma interface
#pragma interface "subdir/objects.h"
オブジェクトのクラスを使うオブジェクト・ファイルのほとんどで領域を節約するには、 そのオブジェクト・クラスを定義するヘッダ・ファイルの中でこの指示子を使ってください。 通常は、 クラス定義を組み込む個々のオブジェクト・ファイルの中に、 いくつかの情報 (インライン・メンバ関数、 デバッグ情報、 仮想関数を実装する内部テーブルの予備的なコピー) の局所的なコピーが保持されなければなりません。 このような情報の複製を回避するために、 このプラグマを使うことができます。 コンパイル時に`#pragma interface'を含むヘッダ・ファイルが組み込まれると、 (そのヘッダ・ファイルを組み込んだ入力ソース・ファイル自体が `#pragma implementation'を使っていなければ) こうした予備的な情報は生成されません。 その代わりに、 オブジェクト・ファイルの中には、 リンク時に解決されることになる参照が含まれることになります。 この指示子の2番目の形式は、 異なるディレクトリに同じ名前のヘッダ・ファイルが複数存在するような場合に役に立ちます。 この形式を使う場合には、 `#pragma implementation'に対しても同一の文字列を指定しなければなりません。
#pragma implementation
#pragma implementation "objects.h"
組み込まれるヘッダ・ファイルの内容を完全に出力させたい (そしてそれを広域的に可視にしたい) 場合に、 ヘッダ・ファイルを組み込む入力ファイルの中でこのプラグマを使ってください。 これに対して、 組み込まれるヘッダ・ファイルでは`#pragma interface'を使います。 インライン・メンバ関数、 デバッグ情報、 仮想関数を実装するために使われる内部テーブルの予備的なコピーは、 すべて実装ファイルの中に生成されます。 引数を指定せずに`#pragma implementation'を使うと、 ソース・ファイルと同一のベース名 (16) を持つインクルード・ファイルに対して適用されます。 例えば、 `allclass.cc'の中に単に`#pragma implementation'とだけ書くのは、 `#pragma implementation "allclass.h"'と書くのと同じことです。 2.6.0よりも前のバージョンのGNU C++では、 たとえ`#pragma implementation'を指定していなくても、 `allclass.cc'から組み込まれたときにはいつでも、 `allclass.h'は実装ファイルとして扱われていました。 これは良いところよりも悪いところの方が多いと判断され、 無効化されました。 明示的に`#pragma implementation'を使うのであれば、 ソース・ファイルの中において影響を受けるヘッダ・ファイルを組み込むところよりも前に、 それを書かなければなりません。 複数のヘッダ・ファイルのコードを単一の実装ファイルの中に組み込みたいのであれば、 文字列引数を使ってください (ヘッダ・ファイルを組み込むには`#include'を使わなければなりません。 `#pragma implementation'は、 そのヘッダ・ファイルの用途を指定するだけであり、 実際にそれを組み込むわけではありません)。 1つのヘッダ・ファイルの内容を分割して、 複数の実装ファイルの中へ組み込む方法はありません。

`#pragma implementation'`#pragma interface'は、 関数のインライン展開にも影響を及ぼします。

`#pragma interface'の指定されたヘッダ・ファイルの中でクラスを定義すると、 そのクラスの中で定義されている関数に対して、 明示的なextern宣言と類似した影響が及ぼされ、 コンパイラが、 その関数を独立した関数として定義するためのコードを出力することはありません。 その関数の定義は、 呼び出し側においてインライン展開するためだけに使われます。

これとは逆に、 同じヘッダ・ファイルに対して`#pragma implementation'を宣言するソース・ファイルの中で このヘッダ・ファイルを組み込むと、 コンパイラは、 その関数の独立した関数としてのコードを出力します。 これによって、 ポインタを経由して (あるいは、 インライン展開を使わずにコンパイルされた呼び出し側から) アクセス可能な関数が定義されます。 その関数に対するすべての呼び出しをインライン展開することができるのであれば、 `-fno-implement-inlines'を指定してコンパイルすることによって、 関数としてのコード出力を回避することができます。 ただし、 インライン展開されない呼び出しが1つでもあれば、 リンク・エラーが発生することになります。

テンプレート・インスタンスの存在箇所

C++テンプレートは、 UNIXシステム上において通常手に入る情報よりもさらに多くの情報を 環境が提供してくれることを必要とする最初の言語機能です。 コンパイラとリンカは、 何らかの方法によって、 個々のテンプレートのインスタンスが必要である場合には、 そのインスタンスが実行ファイルの中においてただ一度だけ生成され、 それが必要でない場合には、 そのインスタンスは一度も生成されないようにしなければなりません。 この問題には、 以下においてBorlandモデルおよびCfrontモデルとして言及される、 2つの基本的な解決方法があります。

Borland モデル
Borland C++は、 共通ブロックに相当するコードをリンカに追加することによって、 このテンプレートのインスタンス生成の問題を解決しました。 コンパイラは、 テンプレートのインスタンスを使う個々の翻訳単位の中において、 テンプレートのインスタンスを出力します。 重複するインスタンスはリンカによって破棄され、 1つだけが残されます。 このモデルの利点は、 リンカが考慮しなければならないのがオブジェクト・ファイルだけであるという点です。 オブジェクト・ファイルのほかには、 考慮しなければならない複雑な問題はありません。 このモデルの不利な点は、 テンプレートのソース・コードが繰り返しコンパイルされるため、 コンパイル時間が増加するということです。 このモデルに合わせて書かれたソース・コードでは、 すべてのテンプレートの定義がヘッダ・ファイルの中に含まれる傾向があります。 というのは、 テンプレートのインスタンスを生成するためには、 その定義を参照しなければならないからです。
Cfrontモデル
AT&TのC++トランスレータであるCfrontは、 テンプレート・リポジトリという概念を作り出すことによって、 テンプレートのインスタンス生成の問題を解決しました。 テンプレート・リポジトリとは、 自動的に保守されるある領域のことで、 そこにテンプレートのインスタンスが格納されます。 より現代的なバージョンのリポジトリは以下のように機能します。 コンパイラは、 個々のオブジェクト・ファイルをビルドする際に、 その中にあるすべてのテンプレートの定義とインスタンス生成をリポジトリの中に置きます。 リンク時に、 リンク・ラッパ(link wrapper )がオブジェクトをリポジトリの中に加え、 必要なテンプレートのインスタンスのうち、 その時点までに出力されていなかったものをコンパイルします。 このモデルの利点は、 コンパイル速度がより最適であることと、 システム上のリンカをそのまま使用できることです。 Borlandモデルを実装するためには、 コンパイラの供給元が、 システム上のリンカを独自のリンカで置き換える必要があります。 Cfrontモデルの不利な点は、 複雑さの度合いが非常に大きくなり、 したがってエラーの可能性もまた非常に大きくなるということです。 ソース・コードによっては、 このモデルを使っていることがまったく意識されないということもあるかもしれませんが、 実際上は、 1つのディレクトリを使って複数のプログラムを構築したり、 複数のディレクトリを使って1つのプログラムを構築したりすることは非常に困難になる可能性があります。 このモデルに合わせて書かれたソース・コードでは、 インライン展開されないメンバ・テンプレートの定義を別のファイルに分離する傾向があります。 このファイルは別にコンパイルしなければならなくなります。

Linux/GNUやSolaris 2のようなELFシステム、 または、 Microsoft Windows上において、 バージョン2.8以降のGNU ldと組み合わせて使っている場合には、 g++はBorlandモデルをサポートしています。 他のシステム上では、 g++はどちらの自動モデルも実装していません。

将来のバージョンのg++では、 混成モデルをサポートする予定です。 この混成モデルでは、 コンパイル時にテンプレート定義が組み込まれているものについては、 コンパイラがインスタンス生成を出力し、 それ以外のものについては、 テンプレート定義とインスタンス生成のコンテキスト情報を、 コンパイラがオブジェクト・ファイルの中に格納します。 リンク・ラッパ(link wrapper)は、 残りのインスタンス生成を行うために、 この情報を必要に応じて取り出してコンパイラを起動します。 その後にリンカが、 重複するインスタンス生成を破棄して1つだけを残します。

混成モデルがサポートされるまでの間は、 テンプレートのインスタンス生成を処理するための選択肢として、 以下のような方法があります。

  1. テンプレートを使うコードを、 `-frepo'を指定してコンパイルします。 コンパイラは、 `.rpo'という拡張子を持つファイルを生成して、 その中に、 対応するオブジェクト・ファイルの中で使われていて、 そのオブジェクト・ファイルの中でインスタンス生成することのできる すべてのテンプレート・インスタンスを一覧にして書き込みます。 リンク・ラッパである`collect2'が、 各インスタンスの生成をどこで行うべきかをコンパイラに対して指示するために `.rpo'ファイルを更新して、 影響を受けるすべてのオブジェクト・ファイルを再ビルドします。 最初のパスを実行後は、 リンク時のオーバーヘッドは無視できるものになります。 コンパイラは、 同じファイルの中にインスタンス生成のコードを置き続けるからです。 これは、 正しく動作するという意味で、 Borlandモデルに合わせて書かれたアプリケーション・コードに対する最良のオプションです。 Cfrontモデルに合わせて書かれたコードは、 インスタンス生成が行われる1つ以上の箇所においてテンプレート定義が利用可能になるように 修正する必要があります。 通常これは、 ただ単に、 個々のテンプレート・ヘッダの末尾に#include <tmethods.cc>と書くだけで済みます。 ライブラリ・コードについては、 必要とされるすべてのテンプレートのインスタンス生成をライブラリ側に提供させたいのであれば、 そのすべてのオブジェクト・ファイルをまとめてリンクすることを試みてください。 リンク自体は失敗するでしょうが、 リンクの副作用で、 インスタンス生成が生成されることになります。 ただし、 複数のライブラリが同一のインスタンス生成を提供しようとすると、 衝突が起こるかもしれないので、 注意してください。 より細かく制御を行うためには、 次のオプションとして説明する明示的なインスタンス生成を使ってください。
  2. 暗黙のうちにテンプレートのインスタンスが生成されないようにするために `-fno-implicit-templates'を指定してソース・コードをコンパイルして、 実際に使用するすべてのインスタンスを明示的に生成します。 この方法では、 他の方法と比較して、 正確にはどのインスタンスが必要であるかという点に関してより多くの知識が要求されますが、 謎めいた部分はより少なく、 より細かく制御することができます。 プログラムの全体にわたって、 明示的なインスタンス生成を散在させることができます。 おそらくインスタンスの生成は、 インスタンスが使われる翻訳単位の中や、 テンプレートそのものが定義されている翻訳単位の中で行われるでしょう。 必要となる明示的なインスタンス生成のすべてを1つの大きなファイルの中に置くこともできますし、 必要となる個々のインスタンスについて以下のような小さなファイルを作り、 それらを集めてテンプレート生成を行うライブラリを作成することもできます。
    #include "Foo.h"
    #include "Foo.cc"
    
    template class Foo<int>;
    template ostream& operator <<
                    (ostream&, const Foo<int>&);
    
    Cfrontモデルに合わせたコードを使っているのであれば、 メンバ・テンプレートの定義を`#include'によって組み込むことのない ファイルをコンパイルする際には、 おそらく`-fno-implicit-templates'を使わずに済ませることができるでしょう。 1つの大きなファイルを使ってインスタンス生成を行うのであれば、 明示的なインスタンス生成により要求した (他のファイルからは要求されない) すべてのインスタンスを、 そもそも明示的な指定などしなくても生成することができるようにするために、 そのファイルを`-fno-implicit-templates'を指定せずにコンパイルすると良いかもしれません。 明示的なインスタンス生成に対する前方宣言や、 テンプレート・クラスのメンバを1つも生成することなく、 そのテンプレート・クラスに対するコンパイラ・サポート・データ (すなわち仮想テーブル) を生成することを可能にするために、 g++ではWorking Paperに概説されているテンプレートのインスタンス生成構文を拡張しています。
    extern template int max (int, int);
    inline template class Foo<int>;
    
  3. g++が自動的なインスタンス生成管理を実装していると想定して、 何も行わない。 Borlandモデルに合わせて書かれたソース・コードは問題なく動きますが、 個々の翻訳単位の中に、 そこで使われる個々のテンプレートのインスタンスがすべて含まれることになります。 大きなプログラムでは、 許容できない量のコードの重複が発生する可能性があります。
  4. テンプレート定義を持つすべてのファイルに`#pragma interface'を書き加えます。 また、 その個々のファイルを`#include'によって組み込む`.C'ファイルの先頭に `#pragma implementation "filename"'を書き加えます。 その後に、 すべてのファイルを`-fexternal-templates'を指定してコンパイルします。 するとテンプレートは、 それを実装する (すなわち、 そのテンプレート定義が存在するファイルに対して `#pragma implementation'を指定する行を含む) 翻訳単位においてのみ展開されます。 ほかのすべてのファイルは外部参照を使います。 運が良ければ、 何もかもうまく行くでしょう。 未定義シンボルのエラーが出るようであれば、 プログラムの中で使われるテンプレートのインスタンスはいずれも、 そのテンプレートを実装するファイルの中で使われていることを確かめる必要があります。 ある特定のインスタンスについては、 そのファイルの中でまったく用途がないのであれば、 最新のC++ Working Paperにある構文を使って、 そのインスタンスを明示的に生成することができます。
    template class A<int>;
    template ostream& operator << (ostream&, const A<int>&);
    
    この方法は、 どちらのモデルに合わせて書かれたソース・コードでも正しく動作します。 Cfrontモデルに合わせて書かれたコードを使っているのであれば、 クラス・テンプレートを含むファイルとそのメンバ・テンプレートを含むファイルは、 同一の翻訳単位の中に実装されていなければなりません。 これとわずかに異なる解決方法に、 フラグ`-falt-external-templates'を使うという方法があります。 このフラグによってテンプレートのインスタンスは、 テンプレートが定義されているファイルを実装する翻訳単位の中ではなく、 テンプレートのインスタンスがその中で最初に生成される ヘッダ・ファイルを実装する翻訳単位の中に出力されるようになります。 このヘッダ・ファイルは、 すべての翻訳単位において同一でなければなりません。 さもないと、 問題が発生するでしょう。 これらのプラグマに関するより詳しい議論については、 1つのヘッダ・ファイルにおける宣言と定義を参照してください。

メンバ関数への結合ポインタからの関数ポインタの抽出

C++では、 メンバ関数へのポインタ(PMF)は、 考え得るあらゆる種類の呼び出しメカニズムを処理するために、 ある種のワイド・ポインタ(wide pointer)を使って実装されています。 PMFは、 `this'ポインタの調整方法に関する情報を保持する必要があり、 また、 ポイントされる関数が仮想関数の場合は、 仮想テーブル(vtable)の存在場所と、 メンバ関数にアクセスするために参照する仮想テーブル内の場所に関する情報も 保持する必要があります。 内部ループ(inner loop)の中でPMFを使っているのであれば、 そのことを再検討をするべきです。 それがやむをえないのであれば、 少々時間を節約するために、 ある与えられたオブジェクトとPMFのペアの代わりとして呼び出すことのできる 関数ポインタを抽出して、 内部ループの中からそれを直接呼び出すことが可能です。

この方法においても、 関数ポインタを経由して呼び出しを行うことに付随する不利な点があることに注意してください。 ほとんどの現代的なアーキテクチャにおいて、 このような呼び出しは、 CPUが持っている分岐予測機能を無効にしてしまいます。 このことは、 通常の仮想関数呼び出しにもあてはまります。

この拡張機能を構文は以下のとおりです。

extern A a;
extern int (A::*fp)();
typedef int (*fptr)(A *);

fptr p = (fptr)(a.*fp);

この拡張機能を使うためには、 `-Wno-pmf-conversions'を指定しなければなりません。

シグニチャを使った型の抽象化

GNU C++では、 キーワードsignatureを使って、 完全な抽象クラスのインターフェイスをデータ型として定義することができます。 シグニチャ・ポインタを使うことによって、 この抽象化された型を実際のクラスに関係付けることができます。 シグニチャを使いたいのであれば、 `-fhandle-signatures'コマンドライン・オプションを指定して GNUコンパイラを実行してください (このオプションを指定すると、 コンパイラは将来の拡張のためにsigofも予約語として扱います)。

概略的には、 シグニチャとは、 クラスの型の抽象化、 または、 クラスのインターフェイスです。 他の言語にも同様の便利な機能を持つものがあります。 C++のシグニチャは、 MLのシグニチャ(signature)、 Haskellの型クラス(type class)、 Modula-2の定義モジュール(definition module)、 Modula-3のインターフェイス・モジュール(interface module)、 Emeraldの抽象型(abstract type)、 Trellis/Owlの型モジュール(type modules)、 Scratchpad IIのカテゴリ(category)、 POOL-Iの型(type)と関係があります。 シグニチャに関するより詳細な議論については、 GeraldBaumgartnerとVincent F. Russoによる Signatures: A Language Extension for Improving Type Abstraction and Subtype Polymorphism in C++ (Tech report CSD--TR--95--051, Dept. of Computer Sciences, Purdue University, August 1995、 若干改善されたバージョンが Software--Practice & Experience, 25(8), pp. 863--889, August 1995 に収められています) を参照してください。 このTech reportは、 anonymous FTPによって ftp.cs.purdue.eduから `pub/gb/Signature-design.ps.gz'というファイル名で入手可能です。

構文的には、 シグニチャの宣言は、 メンバ関数宣言や入れ子になった型宣言の集合です。 例えば、 以下のシグニチャ宣言は、 メンバ関数 `int foo ()'`int bar (int)' を持つ新しい抽象型Sを定義しています。

signature S
{
  int foo ();
  int bar (int);
};

シグニチャ型は実装の定義を含まないので、 シグニチャのインスタンスを直接書くことはできません。 その代わりに、 必要なインターフェイスを持つ任意のクラスへのポインタを シグニチャ・ポインタとして定義することができます。 このようなクラスは、 そのシグニチャ型の実装になります。

あるクラスをSの実装として使うためには、 そのクラスがpublicのメンバ関数 `int foo ()'`int bar (int)' を確かに持っていなければなりません。 そのクラスは、 publicであるか否かにかかわらず、 他のメンバ関数を持つことができます。 シグニチャに宣言されているメンバ関数を提供する限り、 そのシグニチャ型の実装としては適切なクラスです。

例えば、 CがシグニチャSの要件を満足するクラスである (CS準拠する) としましょう。 すると、

C obj;
S * p = &obj;

は、 シグニチャ・ポインタpを定義して、 型Cのあるオブジェクトを指すように初期化します。 メンバ関数呼び出し`int i = p->foo ();'を行うと、 `obj.foo ()'が実行されます。

標準C++においては、 抽象仮想クラスがこれといくらかよく似た機能を提供しています。 抽象仮想クラスの代わりにシグニチャを使うことには、 主に2つの利点があります。

  1. 下位の型への変換が、 継承とは独立したものになります。 あるシグニチャ型Sにおいて宣言されているすべてのメンバ関数が、 別のクラス型もしくはシグニチャ型のTにもある限り、 継承階層がどのようなものであるかに関係なく、 型Tは型Sの下位の型(サブタイプ)になります。 したがって、 クラスの継承階層を反映した型を使うことを強制されるのではなく、 あらゆる継承 (実装) 階層から完全に独立した型の階層を定義することができます。
  2. シグニチャを使うと、 あるシグニチャ型の実装として既存のクラス階層を扱うことができます。 抽象仮想クラスを使っていると、 既存のクラス階層がコンパイル済みの形式でしか利用できない状況では、 運の尽きとなります。 既存のクラス階層の上位に抽象仮想クラスを組み込むことができないからです。 したがって、 抽象仮想クラスの下位の型(サブタイプ)として インターフェイス・クラスを書くことが必要になるでしょう。

シグニチャについては、 もう1つ些細なポイントがあります。 シグニチャの宣言には、 メンバ関数の宣言のほかに、 メンバ関数の定義を含めることができます。 完全な定義を持つシグニチャのメンバ関数は、 デフォルト実装と呼ばれます。 クラスは、 このシグニチャに準拠するために、 この特定のインターフェイスを持つ必要はありません。 例えば、 あるクラスCは、 メンバ関数`int f0 ()'の実装の有無にかかわらず、 以下のシグニチャに準拠することができます。

signature T
{
  int f (int);
  int f0 () { return f (0); };
};

C::f0を定義すれば、 その定義が優先されます。 C::f0が定義されない場合は、 デフォルトの実装であるS::f0が適用されます。


[Contents]   [Back]   [Prev]   [Up]   [Next]   [Forward]