関数ポインターからデータ ポインターへの変換、およびその逆はほとんどのプラットフォームで機能しますが、機能することが保証されていないことを読みました。これはなぜですか?どちらも単にメイン メモリへのアドレスであるため、互換性があるべきではありませんか?
14 に答える
アーキテクチャは、コードとデータを同じメモリに格納する必要はありません。ハーバード アーキテクチャでは、コードとデータはまったく別のメモリに格納されます。ほとんどのアーキテクチャは、同じメモリ内にコードとデータを格納するフォン ノイマン アーキテクチャですが、C は可能な限り、特定のタイプのアーキテクチャのみに制限されません。
一部のコンピューターには、コードとデータ用に別々のアドレス空間があります。そのようなハードウェアでは機能しません。
この言語は、現在のデスクトップ アプリケーションだけでなく、多数のハードウェアに実装できるように設計されています。
C 言語委員会はvoid*
、関数へのポインターになることを決して意図していなかったようです。彼らは、オブジェクトへの一般的なポインターが欲しかっただけです。
C99の根拠は次のように述べています。
6.3.2.3 ポインター
C は現在、幅広いアーキテクチャーに実装されています。これらのアーキテクチャの一部は、整数型のサイズである均一なポインターを備えていますが、最大限に移植可能なコードは、異なるポインター型と整数型の間に必要な対応を想定できません。一部の実装では、ポインターは任意の整数型よりも幅が広い場合もあります。
void*
(“pointer tovoid
”) を汎用オブジェクト ポインタ型として使用することは、C89 委員会の発明です。fread
この型の採用は、( のように) 任意のポインターを静かに変換するか、引数の型が完全に一致しない場合に文句を言う(のように)関数プロトタイプ引数を指定したいという要望によって刺激されましたstrcmp
。関数へのポインターについては何も述べられていません。これは、オブジェクト ポインターおよび/または整数と一致しない可能性があります。
注最後の段落では、関数へのポインターについては何も述べられていません。それらは他の指針とは異なる可能性があり、委員会はそれを認識しています。
MS-DOS、Windows 3.1 以前を覚えている人にとって、答えは非常に簡単です。これらはすべて、コードとデータ ポインターのさまざまな特性の組み合わせで、いくつかの異なるメモリ モデルをサポートしていました。
たとえば、Compact モデル (小さなコード、大きなデータ) の場合:
sizeof(void *) > sizeof(void(*)())
逆に中型モデル (大きなコード、小さなデータ) では:
sizeof(void *) < sizeof(void(*)())
この場合、コードと日付用に別個のストレージはありませんでしたが、2 つのポインター間で変換することはできませんでした (非標準の __near および __far 修飾子を使用しない限り)。
さらに、ポインターが同じサイズであっても、それらが同じものを指しているという保証はありません。DOS のスモール メモリ モデルでは、コードとデータの両方がポインターの近くで使用されますが、それらは異なるセグメントを指していました。そのため、関数ポインターをデータ ポインターに変換しても、関数とはまったく関係のないポインターが得られるわけではないため、そのような変換は使用できません。
void へのポインターは、あらゆる種類のデータへのポインターに対応できると想定されていますが、必ずしも関数へのポインターである必要はありません。一部のシステムでは、データへのポインターと関数へのポインターの要件が異なります (たとえば、データとコードのアドレッシングが異なる DSP があり、MS-DOS の中型モデルはコードに 32 ビット ポインターを使用し、データには 16 ビット ポインターしか使用しませんでした)。 .
ここですでに述べたことに加えて、 POSIX を見るのは興味深いですdlsym()
:
ISO C 標準では、関数へのポインタをデータへのポインタに前後にキャストできる必要はありません。実際、ISO C 標準では、タイプ void * のオブジェクトが関数へのポインターを保持できることを要求していません。ただし、XSI 拡張機能をサポートする実装では、タイプ void * のオブジェクトが関数へのポインターを保持できる必要があります。ただし、関数へのポインターを別のデータ型 (void * を除く) へのポインターに変換した結果は、まだ定義されていません。ISO C 標準に準拠するコンパイラは、次のように void * ポインターから関数ポインターへの変換が試行された場合に警告を生成する必要があることに注意してください。
fptr = (int (*)(int))dlsym(handle, "my_function");
ここに記載されている問題により、将来のバージョンでは関数ポインターを返す新しい関数が追加されるか、現在のインターフェイスが非推奨になり、データ ポインターを返す関数と関数ポインターを返す関数の 2 つの新しい関数が使用される可能性があります。
C++11 には、 に関する C/C++ と POSIX の間の長年の不一致に対する解決策がありdlsym()
ます。reinterpret_cast
実装がこの機能をサポートしている限り、関数ポインタをデータ ポインタとの間で変換するために使用できます。
標準から、5.2.10 パラ。8、「関数ポインターからオブジェクトポインター型への変換、またはその逆の変換は、条件付きでサポートされています。」1.3.5 は、「条件付きサポート」を「実装がサポートする必要のないプログラム構造」と定義しています。
ターゲット アーキテクチャによっては、コードとデータが、基本的に互換性のない、物理的に異なるメモリ領域に格納される場合があります。
それらは、スペース要件が異なるさまざまなタイプにすることができます。1 つに代入すると、ポインターの値が不可逆的にスライスされる可能性があるため、代入を戻すと結果が異なります。
標準は、必要のないときにスペースを節約する実装を制限したくないため、またはサイズが原因でCPUがそれを使用するために余分ながらくたをしなければならない場合など.
別の解決策:
POSIXが関数とデータポインターが同じサイズと表現を持つことを保証すると仮定すると(これに関するテキストは見つかりませんが、引用されたOPの例は、少なくともこの要件を作成することを意図していることを示唆しています)、次のように動作するはずです:
double (*cosine)(double);
void *tmp;
handle = dlopen("libm.so", RTLD_LAZY);
tmp = dlsym(handle, "cos");
memcpy(&cosine, &tmp, sizeof cosine);
char []
これにより、すべてのタイプのエイリアスを許可されている表現を通過することで、エイリアス規則に違反することを回避できます。
さらに別のアプローチ:
union {
double (*fptr)(double);
void *dptr;
} u;
u.dptr = dlsym(handle, "cos");
cosine = u.fptr;
しかし、memcpy
絶対に 100% 正しい C が必要な場合は、このアプローチをお勧めします。
未定義は、必ずしも許可されていないことを意味するわけではありません。コンパイラの実装者が、必要に応じてより自由に実行できることを意味する可能性があります。
たとえば、一部のアーキテクチャではそれができない場合があります。 undefined を使用すると、これができない場合でも、準拠した 'C' ライブラリを使用できます。
真に移植可能な唯一の解決策はdlsym
、関数に使用するのではなく、dlsym
関数ポインターを含むデータへのポインターを取得するために使用することです。たとえば、ライブラリで次のようにします。
struct module foo_module = {
.create = create_func,
.destroy = destroy_func,
.write = write_func,
/* ... */
};
そして、あなたのアプリケーションで:
struct module *foo = dlsym(handle, "foo_module");
foo->create(/*...*/);
/* ... */
ちなみに、これは良い設計手法であり、dlopen
動的リンクをサポートしていないシステム、またはユーザー/システムインテグレーターが動的リンクを使用したくないシステム上のすべてのモジュールを介した動的読み込みと静的リンクの両方を簡単にサポートできます。
関数ポインターのサイズがデータ ポインターと異なる場合がある最新の例: C++ クラス メンバー関数ポインター
https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/から直接引用
class Base1 { int b1; void Base1Method(); }; class Base2 { int b2; void Base2Method(); }; class Derived : public Base1, Base2 { int d; void DerivedMethod(); };
現在、2 つの可能な
this
ポインターがあります。のメンバー関数へのポインターは、 のメンバー関数
Base1
へのポインターとして使用できます。Derived
どちらも同じthis
ポインターを使用するためです。ただし、 のメンバ関数へのポインタは、 ポインタを調整する必要があるためBase2
、 のメンバ関数へのポインタとしてそのまま使用することはできません。Derived
this
これを解決する方法はたくさんあります。Visual Studio コンパイラがそれを処理する方法を次に示します。
多重継承クラスのメンバー関数へのポインターは、実際には構造体です。
[Address of function] [Adjustor]
多重継承を使用するクラスのメンバー関数へのポインターのサイズは、ポインターのサイズに
size_t
.
tl;dr: 多重継承を使用する場合、メンバー関数へのポインターは (コンパイラー、バージョン、アーキテクチャーなどによって) 実際には次のように格納される場合があります。
struct {
void * func;
size_t offset;
}
これは明らかに a よりも大きいですvoid *
。
ほとんどのアーキテクチャでは、すべての通常のデータ型へのポインターは同じ表現を持つため、データ ポインター型間のキャストは何もしません。
ただし、関数ポインターは別の表現を必要とする可能性があり、おそらく他のポインターよりも大きい可能性があります。void* が関数ポインターを保持できる場合、これは void* の表現がより大きなサイズでなければならないことを意味します。また、void* との間のデータ ポインターのすべてのキャストでは、この余分なコピーを実行する必要があります。
誰かが言ったように、これが必要な場合は、ユニオンを使用して実現できます。しかし、void* のほとんどの用途はデータのためだけであるため、関数ポインターを格納する必要がある場合に備えて、すべてのメモリ使用量を増やすのは面倒です。
これは2012年以来コメントされていないことを知っていますが、そのアーキテクチャの呼び出しは特権をチェックし、追加情報を運ぶため、データと関数のポインタが非常に互換性がないアーキテクチャを知っていることを追加すると便利だと思いました。キャストの量は役に立ちません。ザ・ミルです。