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

Общий фольклор говорит, что:

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

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

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

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


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

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

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

15 ответов

Решение

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

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

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

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

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

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

Хранение двусвязного списка с использованием половины пространства

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

int* my_pointer;

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

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

Следует признать, что (char *) - мой обычный метод, так как он имеет больше смысла.

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

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

Библиотека boost UUID использует этот прием и некоторые другие.

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

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