Когда целочисленный <-> указатель приведен правильно? - PullRequest
77 голосов
/ 22 августа 2011

Народный фольклор говорит, что:

  • Система типов существует по причине. Целые числа и указатели являются различными типами, приведение между ними в большинстве случаев является неправильной практикой, может указывать на ошибку проектирования и ее следует избегать.

  • Даже когда выполняется такое приведение, не следует делать никаких предположений о размере целых чисел и указателей (приведение void* к int - это самый простой способ заставить код не работать на x64), и вместо этого из int следует использовать intptr_t или uintptr_t из stdint.h.

Зная это, когда на самом деле полезно выполнять такие броски?

(Примечание: наличие немного более короткого кода для цены переносимости не считается «действительно полезным».)


Один известный мне случай:

  • Некоторые многопроцессорные алгоритмы без блокировки используют тот факт, что указатель с 2-байтовым выравниванием имеет некоторую избыточность. Затем они используют младшие биты указателя, например, в качестве логических флагов. С процессором, имеющим соответствующий набор команд, это может устранить необходимость в механизме блокировки (который был бы необходим, если бы указатель и логический флаг были разделены).
    (Примечание. Эту практику можно даже безопасно выполнить в Java с помощью java.util.concurrent.atomic.AtomicMarkableReference)

Что-нибудь еще?

Ответы [ 15 ]

38 голосов
/ 22 августа 2011

Я иногда приводю указатели к целым числам, когда они каким-то образом должны быть частью хеш-суммы. Также я приведу их к целым числам, чтобы выполнить с ними некоторую битовую обработку в определенных реализациях, где гарантируется, что у указателей всегда остается один или два свободных бита, где я могу кодировать информацию AVL или дерева RB в левых / правых указателях вместо того, чтобы иметь дополнительные член. Но это все настолько специфично для реализации, что я рекомендую никогда не думать об этом как о каком-либо общем решении. Также я слышал, что иногда указатели опасности могут быть реализованы с помощью такой вещи.

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

При работе со встроенными системами (такими как камеры canon, см. Chdk) часто бывают магические адреса, поэтому там часто встречается (void*)0xFFBC5235 или аналогичный

редактирование:

Просто наткнулся (на мой взгляд) на pthread_self(), который возвращает pthread_t, который обычно является typedef к целому числу без знака. Внутренне, хотя это указатель на некоторую структуру потока, представляющую рассматриваемый поток. В общем, он может использоваться в другом месте для непрозрачной ручки.

15 голосов
/ 22 августа 2011

Это может быть полезно при проверке выравнивания типов в целом, так что неправильно выровненная память захватывается с помощью assert, а не просто SIGBUS / SIGSEGV.

например:.

#include <xmmintrin.h>
#include <assert.h>
#include <stdint.h>

int main() {
  void *ptr = malloc(sizeof(__m128));
  assert(!((intptr_t)ptr) % __alignof__(__m128));
  return 0;
}

(В реальном коде я бы не просто играл на malloc, но это иллюстрирует смысл)

12 голосов
/ 22 августа 2011

Сохранение двусвязного списка с использованием половины пробела

A XOR Linked List объединяет следующий и предыдущий указатели в одно значение одинакового размера. Это достигается путем объединения двух указателей вместе, что требует их обращения как целых чисел.

8 голосов
/ 22 августа 2011

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

Если обратный вызов произойдет до того, как функция вернется, вы можете просто передать локальный (автоматический) адрес intпеременная, и все хорошо.Но лучший реальный пример для этой ситуации - pthread_create, где «обратный вызов» выполняется параллельно, и у вас нет гарантии, что он сможет прочитать аргумент через указатель до того, как pthread_create вернется.В этой ситуации у вас есть 3 варианта:

  1. malloc один int и чтение нового потока и free it.
  2. Передача указателя вызывающей стороне-локальная структура, содержащая int и объект синхронизации (например, семафор или барьер) и вызывающая сторона ожидает его после вызова pthread_create.
  3. Приведите int к void * и передайтеэто по значению.

Вариант 3 намного эффективнее, чем любой из других вариантов, оба из которых предусматривают дополнительный шаг синхронизации (для варианта 1 синхронизация выполняется в malloc / freeи почти наверняка потребует некоторых затрат, поскольку потоки распределения и освобождения не совпадают).

8 голосов
/ 22 августа 2011

Одним из примеров является Windows, например, функции SendMessage() и PostMessage().Они принимают HWnd (дескриптор окна), сообщение (целочисленный тип) и два параметра для сообщения: WPARAM и LPARAM.Оба типа параметров являются интегральными, но иногда вы должны передавать указатели, в зависимости от отправляемого сообщения.Тогда вам придется навести указатель на LPARAM или WPARAM.

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

6 голосов
/ 22 августа 2011

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

Быстрый пример: предположим, что у вас есть периферийное устройство таймера ваппаратно, и имеет 2 32-битных регистра:

  • автономный регистр "счетчика тиков", который уменьшается с фиксированной скоростью (например, каждую микросекунду)

  • регистр управления, который позволяет запускать таймер, останавливать таймер, включать прерывание таймера, когда мы уменьшаем счет до нуля и т. Д.

(Примечаниечто реальное периферийное устройство таймера обычно значительно сложнее).

Каждый из этих регистров является 32-битными значениями, а «базовый адрес» периферийного устройства таймера равен 0xFFFF.0000.Вы можете смоделировать аппаратные средства следующим образом:

// Treat these HW regs as volatile
typedef uint32_t volatile hw_reg;

// C friendly, hence the typedef
typedef struct
{
  hw_reg TimerCount;
  hw_reg TimerControl;
} TIMER;

// Cast the integer 0xFFFF0000 as being the base address of a timer peripheral.
#define Timer1 ((TIMER *)0xFFFF0000)

// Read the current timer tick value.
// e.g. read the 32-bit value @ 0xFFFF.0000
uint32_t CurrentTicks = Timer1->TimerCount;

// Stop / reset the timer.
// e.g. write the value 0 to the 32-bit location @ 0xFFFF.0004
Timer1->TimerControl = 0;

Существует 100 вариантов этого подхода, плюсы и минусы которых можно обсуждать вечно, но суть здесь только в том, чтобы проиллюстрировать общее использование приведенияцелое число для указателя.Обратите внимание, что этот код не является переносимым, привязан к конкретному устройству, предполагает, что область памяти не закрыта и т. Д.

3 голосов
/ 22 августа 2011

Бесполезно выполнять такие приведения, если вы не обладаете полным знанием поведения вашей комбинации компилятор + платформа и не хотите ее использовать (один из таких примеров - сценарий с вашим вопросом).

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

2 голосов
/ 22 августа 2011

Когда правильно хранить указатели в целых числах?Это правильно, когда вы воспринимаете это так, как оно есть: использование поведения, специфичного для платформы или компилятора.

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

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

Тем не менее, если ваш «переносной» интерфейс использует целочисленные дескрипторы, целые числа имеют тот же размер, что и указатели нареализация для определенной платформы, и эта реализация использует указатели внутри, почему бы просто не использовать указатели в качестве целочисленных дескрипторов?Простое приведение к целому числу имеет смысл в этом случае, потому что вы исключаете необходимость какой-либо таблицы поиска дескриптора / указателя.

2 голосов
/ 22 августа 2011

Единственный раз, когда я приводил pointer к integer, это когда я хочу сохранить указатель, но единственное доступное мне хранилище - целое число.

1 голос
/ 22 августа 2011

при x64, on может использовать верхние биты указателей для пометки (так как только 47 битов используются для фактического указателя).это отлично подходит для таких вещей, как генерация кода во время выполнения (LuaJIT использует эту технику, которая, согласно комментариям, является древней методикой), для выполнения этой пометки и проверки меток вам необходимо либо приведение, либо union, что в основном составляетто же самое.

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

inline Page* GetPage(void* pMemory)
{
    return &pPages[((UINT_PTR)pMemory - (UINT_PTR)pReserve) >> nPageShift];
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...