web-dev-qa-db-ja.com

C / C ++で関数ポインターとデータポインターに互換性がないのはなぜですか?

関数ポインターをデータポインターに、またはその逆に変換すると、ほとんどのプラットフォームで機能することを読んでいますが、機能することは保証されていません。これはなぜですか?両方とも単にメインメモリへのアドレスであり、したがって互換性があるべきではありませんか?

127
gexicide

アーキテクチャは、コードとデータを同じメモリに保存する必要はありません。ハーバードアーキテクチャでは、コードとデータは完全に異なるメモリに保存されます。ほとんどのアーキテクチャは、同じメモリにコードとデータがあるVon Neumannアーキテクチャですが、Cは可能な限り特定のタイプのアーキテクチャのみに限定しません。

169
Dirk Holsopple

一部のコンピューターには、コードとデータ用に別々のアドレススペースがあります。そのようなハードウェアでは動作しません。

この言語は、現在のデスクトップアプリケーションだけでなく、大規模なハードウェアセットに実装できるように設計されています。


C言語委員会はvoid*は関数へのポインタであり、オブジェクトへの汎用ポインタが必要でした。

C99の理論的根拠:

6.3.2.3ポインター
Cは現在、幅広いアーキテクチャに実装されています。これらのアーキテクチャの一部は整数型のサイズである均一なポインタを備えていますが、最大限に移植可能なコードは、異なるポインタ型と整数型の間の必要な対応を想定できません。実装によっては、ポインターは整数型よりも幅が広い場合があります。

の用法 void*(「voidへのポインター」)は、汎用オブジェクトポインタータイプとして、C89委員会の発明です。このタイプの採用は、(freadのように)任意のポインタを静かに変換するか、引数のタイプが完全に一致しない場合(strcmpのように)文句を言う関数プロトタイプ引数を指定したいという要望に刺激されました。関数へのポインターについては何も言われていません。これは、オブジェクトポインターや整数と一致しない場合があります。

最後の段落の関数へのポインタについては何も言及されていません。それらは他の指針とは異なる可能性があり、委員会はそれを認識しています。

37
Bo Persson

MS-DOS、Windows 3.1以前を覚えている人にとっては、答えは非常に簡単です。これらはすべて、コードとデータポインターの特性のさまざまな組み合わせで、いくつかの異なるメモリモデルをサポートするために使用されていました。

たとえば、コンパクトモデル(小さなコード、大きなデータ)の場合:

sizeof(void *) > sizeof(void(*)())

逆に、中規模モデル(大きなコード、小さなデータ):

sizeof(void *) < sizeof(void(*)())

この場合、コードと日付用の別個のストレージはありませんでしたが、2つのポインター間で変換できませんでした(非標準の__nearおよび__far修飾子を使用する場合を除く)。

さらに、ポインタが同じサイズであっても、同じものを指しているという保証はありません-DOS Smallメモリモデルでは、コードとデータの両方がポインタの近くで使用されますが、異なるセグメントを指していました。したがって、関数ポインターをデータポインターに変換しても、関数とは関係のあるポインターはまったく得られないため、このような変換は使用できませんでした。

30
Tomek

Voidへのポインターは、あらゆる種類のデータへのポインターを収容できるはずですが、必ずしも関数へのポインターではありません。一部のシステムでは、関数へのポインターの要件がデータへのポインターとは異なります(たとえば、データとコードのアドレス指定が異なるDSPがあり、MS-DOSのメディアモデルはコードに32ビットポインターを使用しますが、データには16ビットポインターのみを使用します) 。

23
Jerry Coffin

ここで既に述べたことに加えて、POSIX dlsym() を見るのも興味深いです。

ISO C標準では、関数へのポインターをデータへのポインターに前後にキャストできる必要はありません。実際、ISO C標準では、void *型のオブジェクトが関数へのポインターを保持できることを要求していません。ただし、XSI拡張をサポートする実装では、void *型のオブジェクトが関数へのポインターを保持できる必要があります。ただし、関数へのポインターを別のデータ型へのポインターに変換した結果(void *を除く)は未定義のままです。次のように、void *ポインターから関数ポインターへの変換が試行される場合、ISO C標準に準拠するコンパイラーは警告を生成する必要があることに注意してください。

 fptr = (int (*)(int))dlsym(handle, "my_function");

ここで指摘した問題により、将来のバージョンでは、関数ポインターを返す新しい関数を追加するか、現在のインターフェイスを廃止して、データポインターを返す関数と関数ポインターを返す関数の2つの新しい関数を使用する可能性があります。

12

C++ 11には、dlsym()に関するC/C++とPOSIXの長年の不一致に対する解決策があります。実装がこの機能をサポートしている限り、reinterpret_castを使用して、関数ポインターとデータポインターを変換できます。

標準から、5.2.10 para。 8、「関数ポインターをオブジェクトポインタータイプに、またはその逆に変換することが条件付きでサポートされています。」 1.3.5は、「条件付きでサポートされる」を「実装がサポートする必要のないプログラム構成」と定義しています。

9
David Hammen

ターゲットアーキテクチャによっては、コードとデータが基本的に互換性のない物理的に異なるメモリ領域に保存される場合があります。

7
Graham Borland

undefinedは必ずしも許可されないことを意味するわけではありません。コンパイラの実装者が望んでいる方法でそれを行う自由があることを意味します。

たとえば、一部のアーキテクチャでは不可能な場合があります-undefinedを使用すると、たとえこれを実行できなくても、準拠する「C」ライブラリを保持できます。

5
Martin Beckett

スペース要件が異なるさまざまなタイプにすることができます。 1つに割り当てると、ポインターの値が不可逆的にスライスされる可能性があります。そのため、割り当てを戻すと、異なる結果が生じます。

標準は、不要な場合やサイズによってCPUが使用するために余計なことをしなければならない場合など、スペースを節約する可能性のある実装を制限したくないため、それらは異なるタイプになる可能性があると思います...

5
Edward Strange

別の解決策:

POSIXが関数とデータポインターのサイズと表現が同じであることを保証すると仮定します(これに関するテキストは見つかりませんが、引用された例のOPでは、少なくともintendedがこの要件を満たすことを示唆しています)、動作するはずです:

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;

しかし、完全に正しいCが必要な場合は、memcpyアプローチをお勧めします。

4
R..

唯一の真に移植可能なソリューションは、関数に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を介した動的ロードと、動的リンクをサポートしないシステム上のすべてのモジュールの静的リンク、またはユーザー/システムインテグレーターが望まない場合の両方を簡単にサポートできます動的リンクを使用します。

2
R..

ほとんどのアーキテクチャでは、すべての通常のデータ型へのポインターは同じ表現を持っているため、データポインター型間のキャストは何もしません。

ただし、関数ポインターは別の表現を必要とする可能性があり、おそらく他のポインターよりも大きい可能性があります。 void *が関数ポインタを保持できる場合、これはvoid *の表現がより大きなサイズでなければならないことを意味します。そして、void *への/からのデータポインターのキャストはすべて、この余分なコピーを実行する必要があります。

誰かが言ったように、これが必要な場合は、ユニオンを使用して実現できます。ただし、void *のほとんどの用途は単なるデータ用であるため、関数ポインターを保存する必要がある場合に備えて、すべてのメモリ使用量を増やすのは面倒です。

2
Barmar

関数ポインターのサイズがデータポインターと異なる場合の最新の例: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 *よりも大きいです。

1
Andrew Sun