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


130

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


16
標準Cでは未定義、POSIXで定義。違いに注意してください。
ephemient

私はこれで少し新しいですが、「=」の右側でキャストを行うべきではありませんか?問題のように思えますが、あなたはvoidポインターに割り当てています。しかし、manページがこれを実行しているので、誰かが私を教育してくれるといいのですが。「dlsymからの戻り値をキャストする人々のネット」の例を見てみましょう。たとえば、こちら:daniweb.com/forums/thread62561.html
JasonWoof

9
データ型のセクションでPOSIXが言っていることに注意してください:§2.12.3ポインター型。すべての関数ポインタ型は、への型ポインタと同じ表現を持つ必要がありますvoid。関数ポインタをに変換してもvoid *、表現は変更されません。そのvoid *ような変換の結果の値は、情報を失うことなく、明示的なキャストを使用して、元の関数ポインター型に戻すことができます。:ISO C標準ではこれは必須ではありませんが、POSIXに準拠するには必須です。
ジョナサンレフラー

2
これは、このWebサイトのABOUTセクションの質問です。:) :) ここで質問を参照してください
ZooZ

1
@KeithThompson:世界は変化し、POSIXも変化します。2012年に私が書いたものは、2018年には適用されなくなりました。POSIX標準は、言い回しを変更しました。これは現在関連付けられてdlsym()います。「アプリケーションの使用法」セクションの終わりに注意してください。次のように、void *ポインターから関数ポインターへの 変換fptr = (int (*)(int))dlsym(handle, "my_function"); はISO C標準では定義されていません。この標準では、準拠した実装で正しく機能するためにこの変換が必要です。
ジョナサンレフラー

回答:


171

アーキテクチャでは、コードとデータを同じメモリに格納する必要はありません。ハーバードアーキテクチャでは、コードとデータは完全に異なるメモリに格納されます。ほとんどのアーキテクチャは、コードとデータが同じメモリにあるフォンノイマンアーキテクチャですが、Cは可能な限り特定の種類のアーキテクチャのみに限定していません。


15
また、コードとデータが物理ハードウェアの同じ場所に格納されている場合でも、ソフトウェアとメモリへのアクセスにより、オペレーティングシステムの「承認」なしに、データをコードとして実行できないことがよくあります。DEPなど。
Michael Graczyk 2012

15
少なくとも異なるアドレススペースを持つことと同じくらい重要(おそらくより重要)は、関数ポインターがデータポインターとは異なる表現を持つ可能性があることです。
Michael Burr

14
異なるアドレススペースを使用するコードとデータポインターを使用するためにハーバードアーキテクチャを使用する必要さえありません。古いDOSの「小」メモリーモデルがこれを行いました(の近くのポインターCS != DS)。
caの

1
オペレーティングシステムでコードをどこかに記述できる場合でも、最新のプロセッサでさえ、通常、命令とデータキャッシュは別々に処理されるため、このような混在に苦労します。
PypeBros 2012

3
@EricJ。を呼び出すまでVirtualProtectは、データの領域を実行可能としてマークできます。
ディートリッヒエップ2012

37

一部のコンピューターには、コードとデータ用に個別のアドレススペースがあります(ありました)。このようなハードウェアでは機能しません。

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


C言語委員会はvoid*関数へのポインタになることを決して意図していないようであり、オブジェクトへの一般的なポインタを望んでいただけです。

C99の理論的根拠は次のとおりです。

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

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

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


標準では、データタイプを同じサイズにし、1つに割り当ててから再度割り当てると同じ値になることを保証するだけで、これをいじることなく互換性を持たせることができます。それらは、すべてと互換性のある唯一のポインター型であるvoid *を使用してこれを行います。
Edward Strange

15
@CrazyEddie関数ポインタをに割り当てることはできませんvoid *
ouah

4
void *が関数ポインターを受け入れるのは間違っているかもしれませんが、要点は残っています。ビットはビットです。規格では、異なるタイプのサイズが互いにデータを収容できる必要があり、割り当てが異なるメモリセグメントで使用されている場合でも割り当てが機能することが保証されます。この非互換性が存在する理由は、これは規格によって保証されていないため、割り当てでデータが失われる可能性があるためです。
エドワードストレンジ

5
ただしsizeof(void*) == sizeof( void(*)() )、関数ポインタとデータポインタのサイズが異なる場合は、必要な領域が無駄になります。これは、最初のC標準が作成された80年代の一般的なケースでした。
Robᵩ

8
@RichardChambers:16ビットを命令に、8ビットをデータに使用するAtmel AVRのように、異なるアドレススペースは異なるアドレス幅を持つこともあります。その場合、データ(8ビット)ポインターを関数(16ビット)ポインターに変換し、再び戻すのは困難です。Cは簡単に実装できるはずです。その容易さの一部は、データと命令ポインターを互いに互換性がないままにすることから来ます。
John Bode 2012

30

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

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

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

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

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

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

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


再:「関数ポインターをデータポインターに変換しても、関数とはまったく関係のないポインターが得られないため、そのような変換は使用できませんでした」:これは完全には従いません。をに変換するint*と、void*実際には何もできないポインターが得られますが、変換を実行できると便利です。(これはオブジェクトポインターvoid*を格納できるため、それらが保持する型を知る必要のない一般的なアルゴリズムに使用できるためです。許可されていれば、同じことが関数ポインターにも役立ちます。)
ruakh

4
@ruakh:変換の場合int *にはvoid *void *原稿が同じオブジェクトに少なくとも点が保証されint *た-これは、一般的なアルゴリズムのために有用であるので、そのアクセス先の尖ったからオブジェクト、等int n; memcpy(&n, src, sizeof n);。関数ポインタをに変換しても関数を指すポインタがvoid *生成されない場合、そのようなアルゴリズムには役立ちません。できることはvoid *、関数ポインタに戻すことだけです。そのため、とunionを含むvoid *関数ポインタを使用するだけです。
カフェ

@caf:結構です。ご指摘いただきありがとうございます。そして、そのことについては、してもvoid* いた機能にポイントを、私はそれは、人々がに渡すための悪いアイデアだろうと仮定しますmemcpy。:-P
ruakh

上からコピー:POSIXがデータ型で言っていることに注意してください:§2.12.3ポインタ型。すべての関数ポインタ型は、への型ポインタと同じ表現を持つ必要がありますvoid。関数ポインタをに変換してもvoid *、表現は変更されません。そのvoid *ような変換の結果の値は、情報を失うことなく、明示的なキャストを使用して、元の関数ポインター型に戻すことができます。:ISO C標準ではこれは必須ではありませんが、POSIXに準拠するには必須です。
ジョナサンレフラー

@caf 適切なタイプを知っているコールバックに渡すだけの場合は、変換の安全性だけに関心があり、変換された値が持つ可能性のある他の関係には関心がありません。
デュプリケータ

23

voidへのポインターは、あらゆる種類のデータへのポインターに対応できるようになっていますが、必ずしも関数へのポインターではありません。一部のシステムでは、関数へのポインターの要件がデータへのポインターとは異なります(たとえば、データとコードのアドレッシングが異なるDSPがあります。MS-DOSの中型モデルでは、コードに32ビットポインターを使用しましたが、データには16ビットポインターしか使用していません)。 。


1
ただし、dlsym()関数がvoid *以外のものを返すべきではありません。つまり、void *が関数ポインターに十分な大きさでない場合、すでに不愉快ではありませんか?
Manav、2011

1
@Knickerkicker:はい、たぶん。メモリが機能する場合、dlsymからの戻り値型は、おそらく9または10年前にOpenGroupのメーリングリストで詳細に議論されました。そもそも、何が起こったのか覚えていません。
ジェリーコフィン

1
あなたが正しい。これは、(古くはありますが)かなり良い要約です。
Manav、2011


2
@LegoStormtroopr:21人が賛成投票のアイデアに同意する方法は興味深いですが、実際に賛成したのは約3人だけです。:-)
Jerry Coffin 2014

13

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

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

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

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


これは、dlsymを使用して関数のアドレスを取得することが現在安全でないことを意味しますか?現在、安全な方法はありますか?
殺虫剤2012

4
つまり、現在のPOSIXでは、プラットフォームABIから、関数ポインタとデータポインタの両方を安全にキャストvoid*およびバックキャストできる必要があります。
Maxim Egorushkin 2012

@gexicideこれは、POSIX準拠の実装が言語を拡張したことを意味し、標準で定義されていない動作自体に実装定義の意味を与えます。これは、C99標準の一般的な拡張機能の1つであるセクションJ.5.7関数ポインターキャストにもリストされています。
David Hammen 2012

1
@DavidHammenこれは言語の拡張ではなく、新しい追加要件です。Cはvoid*関数ポインタと互換性がある必要はありませんが、POSIXは互換性があります。
Maxim Egorushkin 2012

9

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

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


できますができません。適合コンパイラ、そのための警告を生成する必要があります(これにより、エラーがトリガーされるはず-Werrorです)。より優れた(そして非UB)ソリューションは、によって返されたオブジェクトへのポインタを取得しdlsym(つまりvoid**)、それを関数ポインタへのポインタに変換することです。まだ実装定義されていますが、警告/エラーの原因にはなりません
Konrad Rudolph

3
@KonradRudolph:そう思わない。「条件付きでサポートされる」表現はdlsymGetProcAddress警告なしにコンパイルできるように特別に記述されています。
MSalters 2012

@MSalters「同意しない」とはどういう意味ですか?私は正しいか間違っています。dlsymをドキュメントは明示的に述べている「ISO C規格に準拠したコンパイラは、関数ポインタへのvoid *型のポインタからの変換が試行された場合に警告を生成するために必要とされています」。これは推測の余地をあまり残しません。そして、GCC(と-pedantic警告します。繰り返しになりますが、憶測はありません。
Konrad Rudolph

1
フォローアップ:私は今理解していると思います。UBではありません。これは実装定義です。警告を生成する必要があるかどうかはまだわかりませんが、おそらく生成されません。しかたがない。
Konrad Rudolph

2
@KonradRudolph:私はあなたの「すべきではない」という意見に反対しました。回答ではC ++ 11について具体的に言及しており、この問題が解決された当時はC ++ CWGのメンバーでした。C99は確かに異なる表現をしており、条件付きでサポートされているのはC ++の発明です。
MSalters 2012

7

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


「物理的に異なる」とは思いますが、「基本的に互換性がない」という区別について詳しく説明していただけますか。質問で述べたように、はどのポインター型よりも大きいはずのvoidポインターではありません-または、私の側では間違った推定です。
Manav、2011

@KnickerKicker:void *データポインターを保持するのに十分な大きさですが、必ずしも関数ポインターを保持する必要はありません。
ephemient

1
未来へのバック:P
SSpoke

5

undefinedは必ずしも許可されていないことを意味するのではなく、コンパイラの実装者が自由に実行できることを意味します。

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


5

別の解決策:

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;

しかし、memcpy100%正確なCが必要な場合は、このアプローチをお勧めします。


5

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

標準ではスペースが不要な場合や、サイズが原因でCPUが余計な処理を行わなければならない場合などに、スペースを節約する実装の可能性を制限したくないため、タイプが異なる可能性があると思います。


3

唯一の真にポータブルなソリューションは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
いいね!これは保守しやすいように思えますが、これに加えて静的リンクをどのように処理するかは(私には)まだ明らかではありません。詳しく説明できますか?
Manav、2011

2
各モジュールが独自のfoo_module構造(一意の名前を持つ)の場合、配列struct { const char *module_name; const struct module *module_funcs; }と単純な関数を使用して追加のファイルを作成し、このテーブルで「ロード」するモジュールを検索して正しいポインタを返し、これを使用できます。代わりに、dlopendlsym
R .. GitHub ICE HELPING ICEを停止する

@R ..真ですが、モジュール構造を維持する必要があるため、維持費が追加されます。
user877329 2014年

3

関数ポインターとデータポインターのサイズが異なる最新の例: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ポインターがあります。

のメンバー関数へのポインターBase1Derived、両方が同じthis ポインターを使用するため、のメンバー関数へのポインターとして使用できます。ただし、のメンバー関数Base2Derivedthis ポインターは、を調整する必要があるため、のメンバー関数へのポインターとしてそのまま使用することはできません。

これを解決するには多くの方法があります。Visual Studioコンパイラがそれを処理する方法を以下に示します。

多重継承クラスのメンバー関数へのポインターは、実際には構造です。

[Address of function]
[Adjustor]

多重継承を使用するクラスのメンバー関数へのポインターのサイズは、ポインターのサイズにのサイズを加えたものですsize_t

tl; dr:多重継承を使用する場合、メンバー関数へのポインタは(コンパイラ、バージョン、アーキテクチャなどに応じて)実際には次のように格納されます。

struct { 
    void * func;
    size_t offset;
}

これは明らかにより大きいですvoid *


2

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

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

誰かが述べたように、これが必要な場合は、共用体を使用して実現できます。ただし、void *のほとんどの使用はデータのためだけなので、関数ポインタを格納する必要がある場合に備えて、すべてのメモリ使用量を増やすのは面倒です。


-1

私はこれが2012年以降コメントされていないことを知っていますが、そのアーキテクチャの呼び出しが特権をチェックして追加の情報を運ぶため、データと関数に対して非常に互換性のないポインターを持っているアーキテクチャを知っていることを追加することは有益だと思いました。キャストの量は役に立ちません。それはミルです。


この答えは間違っています。たとえば、関数ポインターをデータポインターに変換して読み取ることができます(通常、そのアドレスから読み取る権限がある場合)。結果は、x86などと同じくらい意味があります。
マヌエルジェイコブ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.