関数ポインターをデータポインターに変換したり、その逆を行ったりすると、ほとんどのプラットフォームで機能することを確認しましたが、機能するかどうかは保証されていません。これはなぜですか?どちらも単にメインメモリへのアドレスであってはならず、したがって互換性があるべきではないのですか
関数ポインターをデータポインターに変換したり、その逆を行ったりすると、ほとんどのプラットフォームで機能することを確認しましたが、機能するかどうかは保証されていません。これはなぜですか?どちらも単にメインメモリへのアドレスであってはならず、したがって互換性があるべきではないのですか
回答:
アーキテクチャでは、コードとデータを同じメモリに格納する必要はありません。ハーバードアーキテクチャでは、コードとデータは完全に異なるメモリに格納されます。ほとんどのアーキテクチャは、コードとデータが同じメモリにあるフォンノイマンアーキテクチャですが、Cは可能な限り特定の種類のアーキテクチャのみに限定していません。
CS != DS
)。
VirtualProtect
は、データの領域を実行可能としてマークできます。
一部のコンピューターには、コードとデータ用に個別のアドレススペースがあります(ありました)。このようなハードウェアでは機能しません。
この言語は、現在のデスクトップアプリケーションだけでなく、大規模なハードウェアセットに実装できるように設計されています。
C言語委員会はvoid*
関数へのポインタになることを決して意図していないようであり、オブジェクトへの一般的なポインタを望んでいただけです。
C99の理論的根拠は次のとおりです。
6.3.2.3ポインタ
Cは、幅広いアーキテクチャに実装されています。これらのアーキテクチャのいくつかは、整数型のサイズである均一なポインタを備えていますが、最大限に移植可能なコードは、異なるポインタ型と整数型の間の必要な対応を想定できません。一部の実装では、ポインタは整数型よりも広くなる場合があります。ジェネリックオブジェクトポインタータイプとしての
void*
(「へのポインターvoid
」)の使用は、C89委員会の発明です。この型の採用は、任意のポインターを静かに変換する(のようにfread
)関数のプロトタイプ引数を指定するか、引数の型が完全に一致しない場合(のように)に文句を言う関数プロトタイプ引数を指定したいという欲求によって刺激されましたstrcmp
。関数へのポインターについては何も言われていません。これは、オブジェクトポインターや整数と一致しない場合があります。
注最後の段落では、関数へのポインターについては何も述べられていません。それらは他の指針とは異なる可能性があり、委員会はそれを認識しています。
void *
。
sizeof(void*) == sizeof( void(*)() )
、関数ポインタとデータポインタのサイズが異なる場合は、必要な領域が無駄になります。これは、最初のC標準が作成された80年代の一般的なケースでした。
MS-DOS、Windows 3.1以前を覚えている人にとっては、答えは非常に簡単です。これらはすべて、コードとデータポインターの特性のさまざまな組み合わせで、いくつかの異なるメモリモデルをサポートするために使用されていました。
たとえば、コンパクトモデル(小さなコード、大きなデータ)の場合:
sizeof(void *) > sizeof(void(*)())
逆に中規模モデル(大きなコード、小さなデータ)では:
sizeof(void *) < sizeof(void(*)())
この場合、コードと日付用の個別のストレージはありませんでしたが、2つのポインター間で変換できませんでした(非標準の__nearおよび__far修飾子を使用しない場合)。
さらに、ポインタが同じサイズであっても、ポインタが同じものを指しているという保証はありません。DOSのスモールメモリモデルでは、コードとデータの両方がポインタの近くで使用されていますが、異なるセグメントを指していました。そのため、関数ポインターをデータポインターに変換しても、関数との関係を持つポインターはまったく得られないため、そのような変換は使用できませんでした。
int*
と、void*
実際には何もできないポインターが得られますが、変換を実行できると便利です。(これは、オブジェクトポインターvoid*
を格納できるため、それらが保持する型を知る必要のない一般的なアルゴリズムに使用できるためです。許可されていれば、同じことが関数ポインターにも役立ちます。)
int *
にはvoid *
、void *
原稿が同じオブジェクトに少なくとも点が保証されint *
た-これは、一般的なアルゴリズムのために有用であるので、そのアクセス先の尖ったからオブジェクト、等int n; memcpy(&n, src, sizeof n);
。関数ポインタをに変換しても関数を指すポインタがvoid *
生成されない場合、そのようなアルゴリズムには役立ちません。できることはvoid *
、関数ポインタに戻すことだけです。そのため、とunion
を含むvoid *
関数ポインタを使用するだけです。
void*
いた機能にポイントを、私はそれは、人々がに渡すための悪いアイデアだろうと仮定しますmemcpy
。:-P
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つが追加される可能性があります。
void*
およびバックキャストできる必要があります。
void*
関数ポインタと互換性がある必要はありませんが、POSIXは互換性があります。
C ++ 11には、C / C ++とPOSIXの間の長期にわたる不一致に対する解決策がありdlsym()
ます。reinterpret_cast
実装がこの機能をサポートしている限り、を使用して、関数ポインターをデータポインターとの間で変換できます。
標準から、5.2.10パラ。8、「関数ポインターをオブジェクトポインター型に、またはその逆に変換することは条件付きでサポートされています。」1.3.5は、「条件付きでサポートされる」を「実装がサポートする必要のないプログラム構造」として定義しています。
-Werror
です)。より優れた(そして非UB)ソリューションは、によって返されたオブジェクトへのポインタを取得しdlsym
(つまりvoid**
)、それを関数ポインタへのポインタに変換することです。まだ実装定義されていますが、警告/エラーの原因にはなりません。
dlsym
、GetProcAddress
警告なしにコンパイルできるように特別に記述されています。
-pedantic
)は警告します。繰り返しになりますが、憶測はありません。
ターゲットアーキテクチャによっては、コードとデータは、基本的に互換性がなく、物理的に異なるメモリ領域に格納される場合があります。
void *
データポインターを保持するのに十分な大きさですが、必ずしも関数ポインターを保持する必要はありません。
undefinedは必ずしも許可されていないことを意味するのではなく、コンパイラの実装者が自由に実行できることを意味します。
たとえば、一部のアーキテクチャでは不可能かもしれません-undefinedを使用すると、これを実行できない場合でも、準拠する「C」ライブラリを保持できます。
別の解決策:
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が必要な場合は、このアプローチをお勧めします。
それらは、さまざまなスペース要件を持つさまざまなタイプにすることができます。1つに割り当てると、ポインタの値を不可逆的にスライスできるため、割り当てを戻すと別の結果になります。
標準ではスペースが不要な場合や、サイズが原因でCPUが余計な処理を行わなければならない場合などに、スペースを節約する実装の可能性を制限したくないため、タイプが異なる可能性があると思います。
唯一の真にポータブルなソリューションは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
動的リンクをサポートしていないシステム、またはユーザー/システムインテグレーターが動的リンクを使用したくない場合は、すべてのモジュールを介した動的ロードと静的リンクの両方を簡単にサポートできます。
foo_module
構造(一意の名前を持つ)の場合、配列struct { const char *module_name; const struct module *module_funcs; }
と単純な関数を使用して追加のファイルを作成し、このテーブルで「ロード」するモジュールを検索して正しいポインタを返し、これを使用できます。代わりに、dlopen
とdlsym
。
関数ポインターとデータポインターのサイズが異なる最新の例: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;
}
これは明らかにより大きいですvoid *
。
ほとんどのアーキテクチャでは、すべての通常のデータ型へのポインタは同じ表現を持っているため、データポインタ型間のキャストは何もしません。
ただし、関数ポインターには別の表現が必要になる可能性があると考えられ、おそらく他のポインターよりも大きくなります。void *が関数ポインタを保持できる場合、これはvoid *の表現をより大きなサイズにする必要があることを意味します。そして、void *へ/からのデータポインターのすべてのキャストは、この追加のコピーを実行する必要があります。
誰かが述べたように、これが必要な場合は、共用体を使用して実現できます。ただし、void *のほとんどの使用はデータのためだけなので、関数ポインタを格納する必要がある場合に備えて、すべてのメモリ使用量を増やすのは面倒です。