web-dev-qa-db-ja.com

関数ポインタを(void *)にキャストできないのはなぜですか?

文字列、文字列の配列、およびポインタの配列を受け取り、文字列の配列内で文字列を検索し、ポインタの配列から対応するポインタを返す関数があります。これをいくつかの異なる目的で使用するため、ポインター配列は(void *)の配列として宣言され、呼び出し元は実際にどの種類のポインターが存在するか(したがって、どの種類のポインターが戻り値として返されるか)を知っている必要があります。 )。

ただし、関数ポインタの配列を渡すと、-Wpedanticでコンパイルすると警告が表示されます。

clang:

test.c:40:8: warning: assigning to 'voidfunc' (aka 'void (*)(void)') from 'void *' converts
      between void pointer and function pointer [-Wpedantic]

gcc:

test.c:40:8: warning: ISO C forbids assignment between function pointer and ‘void *’ [-Wpedantic]
   fptr = find_ptr("quux", name_list, (void **)ptr_list,

これがテストファイルです。警告にもかかわらず、「quux」は正しく出力されます。

#include <stdio.h>
#include <string.h>

void foo(void)
{
  puts("foo");
}

void bar(void)
{
  puts("bar");
}

void quux(void)
{
  puts("quux");
}

typedef void (* voidfunc)(void);

voidfunc ptr_list[] = {foo, bar, quux};

char *name_list[] = {"foo", "bar", "quux"};

void *find_ptr(char *name, char *names[], void *ptrs[], int length)
{
  int i;

  for (i = 0; i < length; i++) {
    if (strcmp(name, names[i]) == 0) {
      return ptrs[i];
    }
  }
  return NULL;
}

int main() {
  voidfunc fptr;

  fptr = find_ptr("quux", name_list, (void **)ptr_list,
                  sizeof(ptr_list) / sizeof(ptr_list[0]));
  fptr();

  return 0;
}

-Wpedanticでコンパイルしないか、find_ptr関数を関数ポインター用に1回、非関数ポインター用に1回複製する以外に、警告を修正する方法はありますか?私がやろうとしていることを達成するためのより良い方法はありますか?

13
sagittarian

警告を修正することはできません。実際、私の意見では、関数ポインタを他のポインタにキャストすることは違法であるため、ハードエラーになるはずです。これは、C標準の違反だけでなく、コードを作成する実際のエラーであるアーキテクチャが今日存在するためです。うまくいかない。これらのプログラムが他のいくつかのアーキテクチャでひどくクラッシュする場合でも、多くのアーキテクチャがそれを回避するため、コンパイラはそれを許可します。しかし、それは単なる理論上の標準違反ではなく、実際のバグを引き起こすものです。

たとえば、ia64では、関数ポインタは実際には2つの値であり(または少なくとも最後に調べたときに使用されていました)、両方とも共有ライブラリまたはプログラムと共有ライブラリ間で関数呼び出しを行うために必要です。同様に、関数ポインタを関数にキャストして呼び出す一般的な方法は、戻り値を無視することがわかっているため、voidを返す関数へのポインタに値を返します。これは、トラップ値がレジスタにリークする可能性があるため、ia64でも違法です。多くの命令の後で、無関係なコードの一部でクラッシュが発生します。

関数ポインタをキャストしないでください。常にタイプと一致させてください。これは単なる標準的な衒学者ではなく、重要なベストプラクティスです。

13
Art

これは、C標準で長い間破られており、修正されたことのないものです。関数へのポインターやデータへのポインターに使用できる汎用ポインター型はありません。

C89標準以前は、すべてのCコンパイラで異なるタイプのポインタ間の変換が許可されていました。一般に、_char *_は、任意のデータ型または任意の関数を指す汎用ポインタとして使用されていました。 C89は_void *_を追加しましたが、オブジェクトが何であるかを定義せずに、オブジェクトポインタのみを_void *_に変換できるという句を追加しました。 POSIX標準では、_void *_と関数ポインターを安全に前後に変換できるようにすることで、この問題を修正しています。関数ポインタを_void *_に変換し、正しく機能することを期待するコードがたくさん存在します。その結果、ほとんどすべてのCコンパイラーがそれを許可し、正しいコードを生成します。これは、使用できないものとして拒否されなかったコンパイラーであるためです。

厳密に言えば、Cでジェネリックポインターが必要な場合は、_void *_またはvoid (*)()のいずれかを保持できる共用体を定義し、関数ポインターの明示的なキャストを使用する必要があります。呼び出す前に関数ポインタ型を修正してください。

7
Chris Dodd

1つの解決策は、間接参照のレベルを追加することです。これは多くのことに役立ちます。関数へのポインタを格納する代わりに、関数へのポインタを格納するstructへのポインタを格納します。

typedef struct
{
   void (*ptr)(void);
} Func;

Func vf = { voidfunc };

ptrlist[123] = &vf;

等.

6
n.m.

言語弁護士の理由は、「C標準では明示的に許可されていないため」です。 C11 6.3.2.3p1/p8

1。voidへのポインタは、任意のオブジェクトタイプへのポインタとの間で変換できます。任意のオブジェクトタイプへのポインタは、voidへのポインタに変換して元に戻すことができます。結果は元のポインタと同じになります。

8。あるタイプの関数へのポインターを別のタイプの関数へのポインターに変換して、元に戻すことができます。結果は元のポインタと同じになります。変換されたポインターを使用して、参照されている型と互換性のない型の関数を呼び出す場合、動作は定義されていません。

関数はC用語ではnotオブジェクトではないため、関数へのポインターをvoidへのポインターに変換できるものは何もないことに注意してください。したがって、動作はundefinedです。

ただし、_void *_へのキャスト可能性は一般的な拡張機能です。 C11 J.5共通拡張子7

J.5.7関数ポインタキャスト

1。オブジェクトまたはvoidへのポインタを関数へのポインタにキャストして、データを関数として呼び出すことができます(6.5.4 )。

2。関数へのポインターは、オブジェクトまたはvoidへのポインターにキャストでき、関数を検査または変更できるようにします(たとえば、デバッガーによる)(6.5.4)。

これは、たとえばPOSIXで必要です-POSIXには_void *_を返す関数dlsymがありますが、実際には、関数へのポインタまたはオブジェクトへのポインタのいずれかを返します。シンボルが解決されました。


なぜこれが起こるのかについて-実装がそれに同意できれば、C標準では未定義または未指定のものはありません。ただし、voidポインターと関数ポインターが同じ幅であるという仮定が実際に物事を困難にするプラットフォームがありました。これらの1つは、808616ビットリアルモードです。


そして、代わりに何を使用するのですか? 任意の関数ポインタを別の関数ポインタにキャストできるので、どこでも汎用関数ポインタvoid (*)(void)を使用できます。 _void *_関数ポインターの両方が必要な場合は、構造体または和集合を使用するか、_void *_を割り当てて関数ポインターを指すようにするか、次のことを確認する必要があります。コードは、J.5.7が実装されているプラ​​ットフォームでのみ実行されます;)

void (*)()は一部のソースでも推奨されていますが、プロトタイプがないため、現時点では最新のGCCで警告がトリガーされるようです。

3
Antti Haapala

いくつかの変更を加えると、ポインタの会話を回避できます。

#include <stdio.h>
#include <string.h>

void foo(void)
{
    puts("foo");
}

void bar(void)
{
    puts("bar");
}

void quux(void)
{
    puts("quux");
}

typedef void (* voidfunc)(void);

voidfunc ptr_list[] = {foo, bar, quux};

char *name_list[] = {"foo", "bar", "quux"};

voidfunc find_ptr(char *name, char *names[], voidfunc ptrs[], int length)
{
    int i;

    for (i = 0; i < length; i++) {
        if (strcmp(name, names[i]) == 0) {
            return ptrs[i];
        }
    }
    return NULL;
}

int main() {
    voidfunc fptr;

    fptr = find_ptr("quux", name_list, ptr_list,
                    sizeof(ptr_list) / sizeof(ptr_list[0]));
    fptr();

    return 0;
}
1
Aivars

私はこの古い質問に答えています。既存の答えから1つの可能な解決策が欠けているように思われるからです。

コンパイラが変換を禁止する理由は、sizeof(void(*)(void))sizeof(void*)と異なる可能性があるためです。関数をより汎用的にして、任意のサイズのエントリを処理できるようにすることができます。

_void *find_item(char *name, char *names[], void *items, int item_size, int item_count)
{
  int i;

  for (i = 0; i < item_count; i++) {
    if (strcmp(name, names[i]) == 0) {
      return (char*)items + i * item_size;
    }
  }
  return NULL;
}

int main() {
  voidfunc fptr;

  fptr = *(voidfunc*)find_item("quux", name_list, ptr_list,
                              sizeof(ptr_list[0]),
                              sizeof(ptr_list) / sizeof(ptr_list[0]));
  fptr();

  return 0;
}
_

これで、find_entry()関数はアイテムを直接処理する必要がまったくなくなりました。代わりに、配列へのポインターを返すだけであり、呼び出し元は、逆参照する前に、それを関数ポインターへのポインターにキャストできます。

(上記のコードスニペットは、元の質問の定義を前提としています。完全なコードはここでも確認できます: オンラインで試してください!

0
jpa