Когда действует целочисленный ↔ указатель?

В общем фольклоре говорится, что:

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

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

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

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


Один случай, который я знаю:

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

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

Ответ 1

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

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

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

изменить

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

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

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

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

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

Ответ 6

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

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

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

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

Ответ 10

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

О, и не забывайте, что вам нужно назначить и сравнить указатели с NULL, который обычно представляет собой указатель из 0L

Ответ 11

У меня есть одно использование для такой вещи в сетевом идентификаторе объектов. Такой идентификатор объединяет идентификаторы машины (например, IP-адрес), идентификатор процесса и адрес объекта. Для отправки по сокету указательная часть такого идентификатора должна быть помещена в достаточно широкое целое число, так что оно переносит транспорт туда и обратно. Часть указателя интерпретируется только как указатель (= отбрасывается обратно на указатель) в контексте, где это имеет смысл (одна и та же машина, тот же процесс), на других машинах или в других процессах, которые просто служат для различения разных объектов.

То, что нужно иметь, - это существование uintptr_t и uint64_t как целочисленный тип ширины. (Ну работает только на машинах, которые имеют не более 64 адресов:)

Ответ 12

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

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

inline Page* GetPage(void* pMemory)
{
    return &pPages[((UINT_PTR)pMemory - (UINT_PTR)pReserve) >> nPageShift];
}

Ответ 13

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

Например, int указатели:

int* my_pointer;

перемещение my_pointer++ приведет к продвижению 4 байта (в стандартной 32-разрядной системе). Однако перемещение ((int)my_pointer)++ будет продвигать его на один байт.

Это действительно единственный способ выполнить это, за исключением указания указателя на (char *). ((char*)my_pointer)++

По общему признанию, (char *) является моим обычным методом, поскольку имеет смысл.

Ответ 14

Значения указателя также могут быть полезным источником энтропии для посева генератора случайных чисел:

int* p = new int();
seed(intptr_t(p) ^ *p);
delete p;

Библиотека UUID повышения использует этот трюк и некоторые другие.

Ответ 15

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