GNU コンパイラは、
C++ 言語に対して、
以下に示す拡張機能を提供しています
(さらに、
C 言語に対する拡張機能のほとんども、
C++ プログラムの中で使うことができます)。
これらの拡張機能が利用可能であるかどうかをチェックするソース・コードを書きたいのであれば、
C のプログラムの場合と同様の方法によって、
GNU コンパイラであるかどうかをテストすることができます。
それは、
あらかじめ定義されたマクロ __GNUC__
があるかどうかをチェックするという方法です。
特に GNU C++ であるかどうかをテストするには、
__GNUG__
を使うことができます。
(see section `Standard Predefined Macros' in The C Preprocessor)
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
を破棄して
(この際、
消去関数があればそれを呼び出しますが、
消去関数がなければ何も実行しません)、
そのあとで r
を b
で再初期化することによって、
これを処理します。
この拡張機能は主に、 多重定義された(overloaded)演算子を使う人々を補助するために提供されています。 多重定義された演算子を使うと、 関数の引数だけではなく戻り値も制御する必要が大いにあります。 コピー生成関数が性能に対して重大な悪影響をもたらすようなクラスについては (特に、 高速なデフォルトの生成関数が存在する一般的なケースでは)、 これは大きな節約になります。 この拡張機能の不利な点は、 戻り値のデフォルトの生成関数がいつ呼び出されるかを制御できないということです。 それは、 常に最初に呼び出されます。
2つの引数の「最小値」あるいは「最大値」を返してくれる演算子があると非常に便利です。 GNU C++ では、 以下の演算子が利用できます (GNU C では利用できません)。
a <? b
a >? b
これらの演算子は、 通常の C++ においてはプリミティブ(primitive)ではありません。 というのは、 以下の例に示すように、 C++ では2つの値の最小値を返すマクロを使うことができるからです。
#define MIN(X,Y) ((X) < (Y) ? : (X) : (Y))
このあと `int min = MIN (i, j);' のようにして、 変数 i と j の最小値を min にセットします。
しかし、
X
もしくは Y
に副作用があると、
意図されない動作をすることになるかもしれません。
例えば、
MIN (i++, j++)
は、
小さいほうのカウンタ値を2回インクリメントすることになるので、
正常に動作しません。
GNU C の拡張機能によって、
このような問題を回避した安全なマクロを書くことができます
(see section 式の型に対する名前付け)。
しかし、
MIN
や MAX
をマクロとして書くと、
基本的な算術演算に対して関数呼び出しの表記法を使うことを強制されることにもなります。
GNU C++ の拡張機能を使えば、
`int min = i <? j;' のように書くことができます。
<?
と >?
はコンパイラの内部に組み込まれているので、
副作用を持つ式も正しく処理することができます。
`int min = i++ <? j++;' は正しく動作します。
goto
と消去関数
C++ のプログラムでは、
goto
文を安全に使うことができます。
消去関数を必要とする集合体を包含しているブロックの中から外に出るのに goto
文を使うと、
goto
が制御を移す前にその消去関数が実行されます。
しかし、
生成関数を必要とするスコープの内部に入るために goto
を使うことは、
コンパイラが許しません。
C++ のオブジェクト定義はかなり複雑になることがあります。 原則として、 複数のソース・ファイルにわたって使う個々のオブジェクトについて、 ソース・コードの中で必要なものが2つあります。 第1に、 型宣言と関数プロトタイプによってオブジェクトの構造を記述するインターフェイス仕様が必要です。 第2に、 オブジェクトの実装そのものが必要です。 ヘッダ・ファイル中に分離されたインターフェイスの記述を、 実際の実装の記述に正しく対応するよう保守するのは、 退屈な作業になることがあります。 インターフェイス定義と実装定義が分離されると、 両者は正しく対応しなくなるかもしれないので、 これは危険でもあります。
GNU C++ では、 これら2つの目的を実現するのに1つのヘッダ・ファイルだけで済ませることができます。
警告: これを指定するメカニズムは、 現在変更される過渡期にあります。 当面は、 以下に説明する2つの
#pragma
コマンドのどちらかを使わなければなりません。 将来の GNU C++ のリリースでは、 これに代わるメカニズムによって、 これらの#pragma
コマンドは不必要になるでしょう。
まず、ヘッダ・ファイルの中に完全なオブジェクト定義を書きます。
さらに、
そのヘッダ・ファイルのソース・コードに `#pragma interface' という印を付けます。
この印によって、
コンパイラは、
普通のソース・ファイルが #include
によってそのヘッダ・ファイルを組み込んだ場合には、
そのヘッダ・ファイルをインターフェイス仕様としてのみ使うことができるようになります。
完全な実装定義が組み込まれるべきただ1つのソース・ファイルの中では、
そのヘッダ・ファイルの別の用途を指示するために、
名前付け規約もしくは `#pragma implementation' のどちらかを使うことができます。
#pragma interface
#pragma interface "subdir/objects.h"
#pragma implementation
#pragma implementation "objects.h"
`#pragma implementation' と `#pragma interface' は関数のインライン展開にも影響を及ぼします。
`#pragma interface' の指定されたヘッダ・ファイルの中でクラスを定義すると、
そのクラスの中で定義されている関数に対して、
明示的な extern
宣言と類似した影響が及ぼされます。
この場合、
コンパイラが、
その関数を独立したものとして定義するためにバイナリ・コードを出力することはありません。
その関数の定義は、
呼び出し側においてインライン展開するためだけに使われます。
これとは逆に、 同じヘッダ・ファイルに対して `#pragma implementation' を宣言するソース・ファイルの中でこのヘッダ・ファイルを組み込むと、 コンパイラは、 その関数自体のバイナリ・コードを出力します。 これによって、 ポインタを経由して (あるいは、 インライン展開を使わずにコンパイルされた呼び出し側から) アクセス可能な関数が定義されます。 その関数に対するすべての呼び出しをインライン展開することができるのであれば、 `-fno-implement-inlines' を指定してコンパイルすることによって、 関数自体のバイナリ・コードを出力することを回避することができます。 ただし、 インライン展開されない呼び出しが1つでもあれば、 リンクする際にエラーが発生することになります。
C++ テンプレートは、 通常 UNIX システム上において環境が提供してくれる情報よりもさらに多くの情報を環境が提供してくれることを必要とする最初の言語機能です。 コンパイラとリンカは、 何らかの方法によって、 個々のテンプレートのインスタンスが必要である場合には、 そのインスタンスが実行ファイルの中において確かにただ一度だけ生成され、 それが必要でない場合には、 そのインスタンスは確かに1度も生成されないようにしなければなりません。 この問題には、 以下において Borland モデルおよび Cfront モデルとして言及する2つの基本的な解決方法があります。
Linux/GNU や Solaris 2 のような ELF システム、 もしくは、 Microsoft Windows 上において、 バージョン 2.8 以降の GNU ld と一緒に使っている場合には、 g++ は Borland モデルをサポートしています。 他のシステム上では、 g++ はどちらの自動モデルも実装していません。
将来のバージョンの g++ では、 混成モデルをサポートする予定です。 この混成モデルでは、 コンパイル時にテンプレート定義が組み込まれているものについては、 コンパイラがインスタンス生成コードを出力し、 それ以外のものについては、 コンパイラがテンプレート定義とインスタンスのコンテキスト情報をオブジェクト・ファイルの中に格納します。 リンク・ラッパは、 残りのインスタンスを生成するために、 この情報を必要に応じて取り出してコンパイラを起動します。 その後に、 リンカが重複するインスタンス生成コードを破棄して、 1つだけを残します。
混成モデルがサポートされるまでの間は、 テンプレートのインスタンス生成を処理するための選択肢として、 以下のような方法があります。
#include "Foo.h" #include "Foo.cc" template class Foo<int>; template ostream& operator << (ostream&, const Foo<int>&);Cfront モデルに従うソース・コードを使っていれば、 `#include' によってメンバ・テンプレートの定義を組み込むことのないファイルをコンパイルする際には、 おそらく `-fno-implicit-templates' を使わずに済ませることができるでしょう。 インスタンス生成を行うのに1つの大きなファイルを使うのであれば、 明示的なインスタンス生成により要求した (しかし他のファイルからは要求されない) すべてのインスタンスを、 そもそも明示的な指定などしなくても生成することができるようにするために、 その1つのファイルを `-fno-implicit-templates' を指定せずにコンパイルすると良いかもしれません。 明示的なインスタンス生成に対する前方宣言、 テンプレート・クラスのメンバの明示的なインスタンス生成、 また、 テンプレート・クラスのメンバを1つも生成することなく、 そのテンプレート・クラスに対するコンパイラ・サポート・データ (すなわち仮想テーブル) を生成することを可能にするために、 g++ では Working Paper に概説されているテンプレートのインスタンス生成構文を拡張しています。
extern template int max (int, int); template void Foo<int>::f (); inline template class Foo<int>;
template class A<int>; template ostream& operator << (ostream&, const A<int>&);この方法は、 どちらのモデルに合わせて書かれたソース・コードでもうまく機能します。 Cfront モデルに合わせて書かれたコードを使っているのであれば、 クラス・テンプレートを含むファイルとそのメンバ・テンプレートを含むファイルとは、 同一の翻訳単位の中に実装されていなければなりません。 これとわずかに異なる解決方法に、 フラグ `-falt-external-templates' を使うという方法があります。 このフラグによって、 テンプレートのインスタンス生成コードは、 テンプレートが定義されるファイルを実装する翻訳単位の中ではなく、 テンプレートのインスタンスがその中で最初に生成されるヘッダ・ファイルを実装する翻訳単位の中に出力されるようになります。 このヘッダ・ファイルは、 すべての翻訳単位において同一でなければなりません。 さもないと、 問題が発生するでしょう。 これらのプラグマに関するより詳しい議論については、See section 1つのヘッダ・ファイルにおける宣言と定義。
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
の要件を満足するクラスである
(C
が S
に 規格合致する)
としましょう。
すると、
C obj; S * p = &obj;
は、
シグニチャ・ポインタ p
を定義して、
型 C
のあるオブジェクトを指すように初期化します。
メンバ関数呼び出し `int i = p->foo ();' を行うと、
`obj.foo ()' が実行されます。
標準 C++ においては、 抽象仮想クラスがこれといくらかよく似た便利な機能を提供しています。 抽象仮想クラスの代わりにシグニチャを使うことには、 主に2つの利点があります。
S
において宣言されているすべてのメンバ関数が、
別のクラス型もしくはシグニチャ型の T
にもある限り、
継承階層がどのようなものであるかに関係なく、
型 T
は型 S
の下位の型になります。
したがって、
クラスの継承階層を反映した型を使うことを強制されるのではなく、
あらゆる継承
(実装)
階層から完全に独立した型の階層を定義することができます。
シグニチャにはもう1つ些細な点があります。
シグニチャの宣言には、
メンバ関数の宣言のほかに、
メンバ関数の定義を含めることができます。
シグニチャのメンバ関数で完全な定義を持つものは、
デフォルトの実装と呼ばれます。
クラスは、
このシグニチャの規格に合致するために、
この特定のインターフェイスを持つ必要はありません。
例えば、
あるクラス C
は、
メンバ関数 `int f0 ()' を実装していても、
実装していなくても、
以下のシグニチャの規格に合致することができます。
signature T { int f (int); int f0 () { return f (0); }; };
C::f0
を定義すれば、
その定義が優先されます。
C::f0
が定義されない場合は、
デフォルトの実装である S::f0
が適用されます。