Почему я не могу наложить указатель на функцию (void *)?

У меня есть функция, которая берет строку, массив строк и массив указателей и ищет строку в массиве строк и возвращает соответствующий указатель из массива указателей. Поскольку я использую это для нескольких разных вещей, массив указателей объявляется как массив (void *), и вызывающий должен знать, какие именно указатели на самом деле есть (и, следовательно, какой указатель он возвращает в качестве возвращаемого значения).

Однако, когда я передаю массив указателей функций, я получаю предупреждение при компиляции с помощью -Wpedantic:

лязг:

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

НКА:

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

Вы не можете исправить это предупреждение. На самом деле, по-моему, это должна быть жесткая ошибка, поскольку она незаконна бросать указатели на другие указатели, потому что сегодня существуют архитектуры, где это не просто нарушение стандарта C, а фактическая ошибка, которая сделает код не работа. Компиляторы позволяют это, потому что многие архитектуры уходят от него, даже если эти программы сильно вредят на некоторых других архитектурах. Но это не просто теоретическое стандартное нарушение, это то, что вызывает реальные ошибки.

Например, в указателях функций ia64 (или, по крайней мере, используется последний раз, когда я смотрел) фактически два значения, необходимые для выполнения вызовов функций в разделяемых библиотеках или в программе и в общей библиотеке. Аналогичным образом, обычная практика приведения функций и указателей функций к функциям, возвращающим значение, указателю на функцию, возвращающую void, потому что вы знаете, что в любом случае вы проигнорируете возвращаемое значение, также является незаконным на ia64, поскольку это может привести к утечке данных в регистры вызывая сбои в некотором неродственном фрагменте кода многие инструкции позже.

Не используйте указатели функций. Всегда имейте соответствующие типы соответствия. Это не просто стандартная педантичность, это важная передовая практика.

Ответ 2

Одним из решений является добавление уровня косвенности. Это помогает с большим количеством вещей. Вместо сохранения указателя на функцию сохраните указатель на struct, указав указатель на функцию.

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

Func vf = { voidfunc };

ptrlist[123] = &vf;

и др.

Ответ 3

Это то, что давно нарушено в стандарте C и никогда не было исправлено - нет универсального типа указателя, который можно было бы использовать для указателей на функции и указателей на данные.

До стандарта C89 все компиляторы C допускали преобразование между указателями разных типов, а char * обычно использовался в качестве универсального указателя, который может указывать на любой тип данных или любую функцию. C89 добавил void *, но включил в него предложение, что только указатели объектов могут быть преобразованы в void *, даже не определяя, что это за объект. Стандарт POSIX устраняет эту проблему, предписывая, чтобы void * и указатели функций были безопасно конвертируемыми назад и вперед. Существует так много кода, который преобразует указатели функций в void * и ожидает, что он будет работать правильно. В результате почти все компиляторы C по-прежнему допускают это и по-прежнему генерируют правильный код, поскольку любой компилятор, который не был бы отклонен, был бы отклонен как непригодный для использования.

Строго говоря, если вы хотите иметь универсальный указатель в C, вам нужно определить объединение, которое может содержать void * или void (*)() и использовать явное приведение указателя функции к правильному типу указателя на функцию прежде чем позвонить.

Ответ 4

Причина языковой адвокатуры - "потому что стандарт C явно не разрешает это". C11 6.3.2.3p1/p8

1. Указатель на void может быть преобразован в или из указателя на любой тип объекта. Указатель на любой тип объекта может быть преобразован в указатель на void и обратно; результат должен сравниваться равным исходному указателю.

8. Указатель на функцию одного типа может быть преобразован в указатель на функцию другого типа и обратно; результат должен сравниваться равным исходному указателю. Если преобразованный указатель используется для вызова функции, тип которой не совместим с указанным типом, поведение не определено.

Обратите внимание, что функция не является объектом в терминологии Си, поэтому нет ничего, что позволяло бы вам преобразовать указатель на функцию в указатель на void, следовательно, поведение не определено.

Casability to void * является распространенным расширением. C11 J.5 Общие расширения 7:

J.5.7 Приведение указателя функции

1. Указатель на объект или void может быть приведен к указателю на функцию, что позволяет вызывать данные как функцию (6.5.4).

2. Указатель на функцию может быть приведен к указателю на объект или аннулирован, что позволяет проверять или изменять функцию (например, отладчиком) (6.5.4).

Это требуется, например, для POSIX - в POSIX есть функция dlsym которая возвращает void * но фактически она возвращает возвращаемый либо указатель на функцию, либо указатель на объект, в зависимости от типа разрешенного символа.


Что касается того, почему это происходит - ничто в стандарте C не является неопределенным или неопределенным, если реализации могут согласовать это. Однако были и есть платформы, где предположение, что указатель void и указатель на функцию будут иметь одинаковую ширину, действительно усложнит ситуацию. Одним из них является 16-битный реальный режим 8086.


И что тогда использовать вместо этого? Вы по-прежнему можете приводить любой указатель функции к другому указателю на функцию, поэтому вы можете использовать универсальный указатель на функцию void (*)(void) везде. Если вам нужны и void * и указатель на функцию, вы должны использовать структуру или объединение или выделить void * чтобы указывать на указатель на функцию, или убедиться, что ваш код работает только на платформах, где реализован J.5.7;)

void (*)() рекомендуется некоторыми источниками, но сейчас, похоже, в последних GCC выдается предупреждение, потому что у него нет прототипа.

Ответ 5

С некоторыми изменениями вы можете избежать разговоров с указателями:

#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;
}

Ответ 6

Я отвечаю на этот старый вопрос, потому что кажется, что одно из возможных решений отсутствует в существующих ответах.

Причина, по которой компилятор запрещает преобразование, заключается в том, что 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() вообще не нужно напрямую обрабатывать элемент. Вместо этого он просто возвращает указатель на массив, и вызывающая сторона может привести его к указателю на funcpointer перед разыменованием его.

(Приведенный выше фрагмент кода предполагает определения из исходного вопроса. Полный код также можно посмотреть здесь: попробуйте онлайн!)