Почему указатели функций и указатели данных несовместимы в C/C++?

Я читал, что преобразование указателя функции в указатель данных и наоборот работает на большинстве платформ, но не гарантированно работает. Почему это так? Не должны ли оба быть просто адресами в основной памяти и, следовательно, быть совместимыми?

14 ответов

Решение

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

Некоторые компьютеры имеют (имели) отдельные адресные пространства для кода и данных. На таком оборудовании это просто не работает.

Язык предназначен не только для современных настольных приложений, но и для того, чтобы его можно было реализовать на большом наборе оборудования.


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

Обоснование C99 говорит:

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

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

Примечание. О указателях функций в последнем абзаце ничего не сказано. Они могут отличаться от других указателей, и комитет об этом знает.

Для тех, кто помнит MS-DOS, Windows 3.1 и старше, ответ довольно прост. Все они использовали для поддержки нескольких различных моделей памяти с различными комбинациями характеристик для кода и указателей данных.

Например, для модели Compact (маленький код, большие данные):

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

и наоборот в модели Medium (большой код, маленькие данные):

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

В этом случае у вас не было отдельного хранилища для кода и даты, но вы все равно не могли конвертировать между двумя указателями (если не считать нестандартных модификаторов __near и __far).

Кроме того, нет гарантии, что даже если указатели имеют одинаковый размер, они указывают на одно и то же - в модели памяти DOS Small, и код, и данные используются рядом с указателями, но они указывают на разные сегменты. Таким образом, преобразование указателя функции в указатель данных не даст вам указатель, который вообще имеет какое-либо отношение к функции, и, следовательно, такое преобразование бесполезно.

Предполагается, что указатели на 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");

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

C++11 имеет решение давнего несоответствия между C/C++ и POSIX в отношении dlsym(), Можно использовать reinterpret_cast преобразовать указатель функции в / из указателя данных, если реализация поддерживает эту функцию.

Из стандарта 5.2.10 абз. 8 "условно поддерживается преобразование указателя функции в тип указателя объекта или наоборот". 1.3.5 определяет "условно поддерживаемый" как "программную конструкцию, которую реализация не обязана поддерживать".

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

Они могут быть разных типов с различными требованиями к пространству. Присвоение одному может необратимо срезать значение указателя, так что присваивание назад приводит к чему-то другому.

Я полагаю, что они могут быть разных типов, потому что стандарт не хочет ограничивать возможные реализации, которые экономят пространство, когда это не нужно или когда размер может заставить ЦП делать дополнительную хрень, чтобы использовать его, и т. Д...

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.

Единственное действительно портативное решение не использовать 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 и статическое связывание всех модулей в системах, которые не поддерживают динамическое связывание, или там, где пользователь / системный интегратор не хочет использовать динамическое связывание.

Современный пример того, как указатели на функции могут отличаться по размеру от указателей на данные: указатели на функции-члены класса 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(); };

Есть два возможных 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* только для данных, поэтому было бы обременительно увеличивать все их использование памяти на случай, если необходимо сохранить указатель на функцию.

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

Другие вопросы по тегам