C++ 移行ガイド

extern“C” 関数へのポインタ

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

extern“C”int f1(int);

リンケージを指定しないと、C++ のリンケージが使用されます。C++ リンケージは、明示的に指定することもできます。

extern“C++”int f2(int);

複数の宣言をグループにまとめることもできます。


  extern“C”{
        int g1(); // C リンケージ
        int g2(); // C リンケージ
        int g3(); // C リンケージ
    } // セミコロンなし

この手法は、標準ヘッダーでも幅広く使用されています。

言語リンケージ

「言語リンケージ」とは、関数の呼び出しに関する方法を意味します。たとえば、引数の場所、戻り値の検出場所の指定などがこれに当たります。言語リンケージを宣言するということは、その言語で関数が記述されないという意味です。言語リンケージを宣言すると、指定した言語で記述されているかのように関数を呼び出すことができます。つまり、C++ 関数が C リンケージを持つように宣言するとは、C 言語で記述された関数から C++ 関数を呼び出せるようにするということです。

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

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

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

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


extern“C”int fc(int) { return 1; }      // fcの C リンケージ
int fcpp(int) { return 1; }               // fcppの C++ リンケージ
// fp1 と fp2 の C++ リンケージ
int (*fp1)(int) = fc;                     //不一致
int (*fp2)(int) = fcpp;                   // OK 
// fp3 と fp4 の C リンケージ
extern“C”int (*fp3)(int) = fc;          // OK 
extern“C”int (*fp4)(int) = fcpp;        //不一致

リンケージに関連して問題が発生した場合は、C リンケージ関数と組み合わせ可能なポインタが C リンケージで宣言され、C++ リンケージ関数と組み合わせられるポインタにリンケージ指定がないか、または、C++ リンケージで宣言されていることを確認してください。


extern“C”{
    int fc(int);
    int (*fp1)(int) = fc; // どちらも C リンケージを持つ
}
int fcpp(int); 
int (*fp2)(int) = fcpp;   // どちらも C++ リンケージを持つ

ポインタと関数が一致しない場合は、関数を包含するコード (ラッパー) を記述することによって、コンパイラのエラーを回避することができます (Solaris では、C と C++ の関数リンケージは同じですが、一般的には、リンケージが一致していないとエラーになります。これは新しい言語規則として適用されているためです)。

次の例では、 composer は、C リンケージを持つ関数へのポインタを取る C 関数です。


extern“C”void composer( int(*)(int) );
extern“C++”int foo(int); 
composer( foo ); // 不一致

関数 foo (C++ リンケージを持つ) を 関数 composer に渡すには、次のように foo に C インタフェースを提供する foo_wrapper という C リンケージ関数を作成します。


extern“C”void composer( int(*)(int) );
extern“C++”int foo(int);
extern“C”int foo_wrapper(int i) { return foo(i); }
composer( foo_wrapper ); // OK

この手法は、コンパイラのエラーを回避するためだけでなく、C と C++ の関数が実際には異なるリンケージを持っている場合にも使用できます。

移植性の低い解決策

サンの実装している C と C++ の関数リンケージはバイナリ互換です。すべての C++ の実装がこうなっているわけではありませんが、比較的共通のことです。互換性がなくなってもかまわないのであれば、キャストを使って C++ リンケージ関数を C リンケージ関数と同じように使用できます。

たとえば静的メンバー関数がよい例です。リンケージに関する C++ 言語の新しい規則が関数の型の一部となるまでは、クラスの静的メンバー関数を C リンケージを持つ関数として扱うのが一般的でした。これによって、クラスメンバー関数のリンケージを宣言できないという制限を回避していました。たとえば、次の例を考えてみましょう。


// 既存のコード
typedef int (*cfuncptr)(int);
extern“C”void set_callback(cfuncptr);
class T {
    ...
    static int memfunc(int);
};
...
set_callback(T::memfunc); // 新しい規則では無効

上記の問題を解決するには、前の項でお勧めしたように T::memfunc を呼び出す関数ラッパーを作成してから、すべての set_callback 呼び出しを変更して T::memfunc の代りにラッパーを使用します。こうすると、完全な移植性を持つ正しいコードになります。

もう 1 つの解決策として、次の例のように多重定義した set_callback 呼び出しを作成して、C++ リンケージを持つ関数を受け取り、元の関数を呼び出すこともできます。


// 変更したコード
extern“C”{
    typedef int (*cfuncptr)(int); // C 関数へのポインタ
    void set_callback(cfuncptr);
}
typedef int (*cppfuncptr)(int); // C++ 関数へのポインタ
inline void set_callback(cppfuncptr f) // 多重定義したもの
    { set_callback((cfuncptr)f); }
class T {
    ...
    static int memfunc(int);
};
...
set_callback(T::memfunc); // 元のコードと同じ

この例では、既存のコードをわずかに変更しただけです。ここには、コールバックを設定する set_callback を新たに追加しました。既存のコードは元の set_callback を呼び出していましたが、ここでは多重定義したものを呼び出し、それが元のものを呼び出します。多重定義したものはインライン関数なので、実行時のオーバーヘッドはまったくありません。

この方法は Sun C++ では動作しますが、すべての C++ の実装で動作するとは限りません。他のシステムでは、C 関数と C++ 関数の呼び出し順序が異なる場合があるからです。

関数のパラメータとしての関数へのポインタ

言語リンケージに関する新しい規則の追加に伴う微妙な問題があります。それは、上記の例の composer 関数のような、パラメータとして関数へのポインタを取る関数の問題です。


extern“C”void composer( int(*)(int) );

言語リンケージに関する規則のうち、変更されていない規則として、言語リンケージを持つ関数が宣言されていて、その後に「同じ関数」が言語リンケージなしで定義されている場合は、前の言語リンケージが適用されるという規則があります。


extern“C”int f(int);
int f(int i) { ... } // “C”リンケージを持つ

上記の関数 f は C リンケージを持ちます。この宣言 (インクルードされるヘッダーファイルに含まれている可能性もある) の後の定義は、リンケージ指定を継承します。しかし、次の例に示すように、この関数が関数へのポインタ型のパラメータを取る場合はどうなるのでしょう。


extern“C”int g( int(*)(int) );
int g( int(*pf)(int) ) { ... } //“C”または“C++”リンケージのどちらか?

古い規則と 4.2 コンパイラでは、このコードには、g という関数が 1 つ存在するだけです。新しい規則では、1 行目は、C リンケージを持つ関数へのポインタを取る、C リンケージを持つ関数 g を宣言し、2 行目は、C++ リンケージを持つ関数へのポインタを取る関数を定義していることになります。2 つの関数は同じではありません。2 つ目の関数は C++ リンケージを持ちます。リンケージは関数へのポインタの型の構成要素であるため、2 つの行は、それぞれが g という名前の多重定義関数を参照します。このため、これらの関数が同じ関数であることに依存するコードは、問題になります。コンパイルまたはリンクが失敗する可能性が非常に高くなります。

プログラミングするときの習慣として、リンケージは、宣言だけでなく、関数の定義でも指定するようにしてください。


extern“C”int g( int(*)(int) );
extern“C”int g( int(*pf)(int) ) { ... }

型に関する混乱は、関数パラメータに typedef を使用することでさらに少なくすることができます。


extern“C”typedef int (*pfc)(int); // Cリンケージ関数へのポインタ
extern“C”int g(pfc);
extern“C”int g(pfc pf) { ... }