GNUコンパイラは、
C++言語に対して、
以下に示す拡張機能を提供しています
(さらに、
C言語に対する拡張機能のほとんども、
C++プログラムの中で使うことができます)。
これらの拡張機能が利用可能であるかどうかをチェックするソース・コードを書きたいのであれば、
Cのプログラムの場合と同様の方法によって、
GNUコンパイラであるかどうかをテストすることができます。
それは、
あらかじめ定義されたマクロ__GNUC__
があるかどうかをチェックするという方法です。
特にGNU C++であるかどうかをテストするには、
__GNUG__
を使うことができます。
(The C Preprocessorの`Standard Predefined Macros'を参照)
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))
変数iとjの最小値をminにセットするには、 `int min = MIN (i, j);'のように使います。
しかし、
X
またはY
に副作用があると、
意図されない振る舞いをすることになるかもしれません。
例えば、
MIN (i++, j++)
は、
小さいほうのカウンタ値を2回インクリメントすることになるので、
正しく動作しません。
GNU Cの拡張機能によって、
このような問題を回避できる安全なマクロを書くことができます
(式の型に対する名前付けを参照)。
しかし、
MIN
やMAX
をマクロとして書くと、
基本的な算術演算に対して関数呼び出しの表記法を使うことを強制されることにもなります。
GNU C++の拡張機能を使えば、
`int min = i <? j;'のように書くことができます。
<?
と>?
はコンパイラの内部に組み込まれているので、
副作用を持つ式も正しく処理することができます。
`int min = i++ <? j++;'は正しく動作します。
goto
と消去関数
C++のプログラムでは、
goto
文を使っても安全です。
消去関数を必要とする集合体を包含しているブロックの中から外に出るのにgoto
文を使うと、
goto
が制御を移す前にその消去関数が実行されます。
しかし、
生成関数を必要とするスコープの内部に入るためにgoto
を使うことは、
コンパイラが許しません。
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 implementation
#pragma implementation "objects.h"
`#pragma implementation'と`#pragma interface'は、 関数のインライン展開にも影響を及ぼします。
`#pragma interface'の指定されたヘッダ・ファイルの中でクラスを定義すると、
そのクラスの中で定義されている関数に対して、
明示的なextern
宣言と類似した影響が及ぼされ、
コンパイラが、
その関数を独立した関数として定義するためのコードを出力することはありません。
その関数の定義は、
呼び出し側においてインライン展開するためだけに使われます。
これとは逆に、 同じヘッダ・ファイルに対して`#pragma implementation'を宣言するソース・ファイルの中で このヘッダ・ファイルを組み込むと、 コンパイラは、 その関数の独立した関数としてのコードを出力します。 これによって、 ポインタを経由して (あるいは、 インライン展開を使わずにコンパイルされた呼び出し側から) アクセス可能な関数が定義されます。 その関数に対するすべての呼び出しをインライン展開することができるのであれば、 `-fno-implement-inlines'を指定してコンパイルすることによって、 関数としてのコード出力を回避することができます。 ただし、 インライン展開されない呼び出しが1つでもあれば、 リンク・エラーが発生することになります。
C++テンプレートは、 UNIXシステム上において通常手に入る情報よりもさらに多くの情報を 環境が提供してくれることを必要とする最初の言語機能です。 コンパイラとリンカは、 何らかの方法によって、 個々のテンプレートのインスタンスが必要である場合には、 そのインスタンスが実行ファイルの中においてただ一度だけ生成され、 それが必要でない場合には、 そのインスタンスは一度も生成されないようにしなければなりません。 この問題には、 以下においてBorlandモデルおよびCfrontモデルとして言及される、 2つの基本的な解決方法があります。
Linux/GNUやSolaris 2のようなELFシステム、 または、 Microsoft Windows上において、 バージョン2.8以降のGNU ldと組み合わせて使っている場合には、 g++はBorlandモデルをサポートしています。 他のシステム上では、 g++はどちらの自動モデルも実装していません。
将来のバージョンのg++では、 混成モデルをサポートする予定です。 この混成モデルでは、 コンパイル時にテンプレート定義が組み込まれているものについては、 コンパイラがインスタンス生成を出力し、 それ以外のものについては、 テンプレート定義とインスタンス生成のコンテキスト情報を、 コンパイラがオブジェクト・ファイルの中に格納します。 リンク・ラッパ(link wrapper)は、 残りのインスタンス生成を行うために、 この情報を必要に応じて取り出してコンパイラを起動します。 その後にリンカが、 重複するインスタンス生成を破棄して1つだけを残します。
混成モデルがサポートされるまでの間は、 テンプレートのインスタンス生成を処理するための選択肢として、 以下のような方法があります。
#include <tmethods.cc>
と書くだけで済みます。
ライブラリ・コードについては、
必要とされるすべてのテンプレートのインスタンス生成をライブラリ側に提供させたいのであれば、
そのすべてのオブジェクト・ファイルをまとめてリンクすることを試みてください。
リンク自体は失敗するでしょうが、
リンクの副作用で、
インスタンス生成が生成されることになります。
ただし、
複数のライブラリが同一のインスタンス生成を提供しようとすると、
衝突が起こるかもしれないので、
注意してください。
より細かく制御を行うためには、
次のオプションとして説明する明示的なインスタンス生成を使ってください。
#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>;
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
の要件を満足するクラスである
(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
が適用されます。