Коли цілочисельний <-> покажчик фактично правильний?


77

Поширений фольклор каже, що:

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

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

Знаючи це, коли насправді корисно виконувати такі касти?

(Примітка: наявність трохи коротшого коду для ціни переносимості не вважається "фактично корисним".)


Я знаю один випадок:

  • Деякі багатопроцесорні алгоритми без блокування використовують той факт, що 2-байтний вирівняний покажчик має деяку надмірність. Потім вони використовують найнижчі біти вказівника як, наприклад, логічні прапори. Якщо процесор має відповідний набір команд, це може усунути потребу в механізмі блокування (який був би необхідний, якщо вказівник та логічний прапор були окремими).
    (Примітка: Цю практику можна навіть безпечно виконувати в Java через java.util.concurrent.atomic.AtomicMarkableReference)

Щось більше?


5
Зіставлення між покажчиком та an intptr_tвизначено реалізацією, тому я б також не використовував алгоритм без блокування, якщо не знав точно, на якому компіляторі він буде працювати.
Андреас Брінк,

6
Кожен алгоритм блокування використовує принаймні деякі специфічні властивості реалізації ...
PlasmaHH

3
@PlasmaHH: Гарна точка. C (і C ++ перед C ++ 11) не має поняття багатопотокових програм або спільної пам'яті програм. Отже, якщо ви користуєтесь алгоритмами без замків, ви вже покладаєтесь на властивості, конкретні для реалізації, але варто пам’ятати про це, оскільки легко забути, що реалізація тут не потрібна для того, щоб робити «нормальну» справу.
Кевін Каткарт,

1
Власне, uintptr_tє в <stdint.h>, або <cstdint>в C ++ 0x. Visual C ++ 2008 помилковий, якщо звідти ви його взяли.
Вільгельмтель

Я не роблю Visual C ++, і це була очевидна помилка мною, дякую! :)
Кос,

Відповіді:


38

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

У деяких ситуаціях мені потрібен унікальний ідентифікатор кожного об’єкта, який я передаю, наприклад, серверам як свій ідентифікатор запиту. Залежно від контексту, коли мені потрібно зберегти трохи пам’яті, і це того варте, я використовую адресу свого об’єкта як такий ідентифікатор, і, як правило, доводиться призначати його цілому числу.

Під час роботи із вбудованими системами (наприклад, у канонах Canon, див. Chdk) часто трапляються магічні аденси, тому там (void*)0xFFBC5235або подібне часто також зустрічається там

редагувати:

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


1
Замість того, щоб перекидати значення покажчиків на цілі числа для хешування, вам слід замість цього просто прочитати їх подання (як unsigned char [sizeof(T *)]) для хешування ...
R .. GitHub СТОП ДОПОМОГАЙ ЛЕД

1
Як зазначає також OP, значення покажчиків часто мають надмірність, так як нижні біти дорівнюють 0. Переміщення їх, а потім множення, наприклад, на 1000000007, часто призводить до дивно добре розподіленого хешу, якого достатньо для деяких моїх додатків. Крім того, я не шанувальник просто сліпого складання бітів і бітів, щоб сформувати хеш, трохи подумавши, швидший хеш, специфічний для домену, можна знайти без зусиль ракетної науки.
PlasmaHH

4
+1 Приємно бачити, як ви розумієте небезпеку того, що ви робите, і пропонуєте іншим не робити цього :-) Я вражений тим, що це оцінено за SO і не отримує купу коментарів "не мікрооптимізувати".
phkahler

5
Я люблю, коли люди, які пишуть мої бібліотеки, оптимізують для мене. Це проблема, коли я витрачаю час на мікрооптимізацію> :)
Steven Schlansker

15

Це може бути корисним при перевірці вирівнювання типів загалом, щоб неправильно вирівняна пам'ять потрапляла на твердження, а не просто на 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, але це ілюструє суть)


13

Зберігання подвійно пов’язаного списку, використовуючи половину місця

XOR Зв'язаний список поєднує в собі такі і ПЕРЕД покажчики в одне значення одного і того ж розміру. Це робиться шляхом поєднання двох покажчиків разом, що вимагає обробки їх як цілих чисел.


1
Забув про це;) Класний хак для критично важливих для пам'яті вбудованих рішень
Кос

Окрім того, що такий вузол неможливо легко видалити зі списку, коли дається лише вказівник на вузол.
Максим Єгорушкін

1
Так, вам, як правило, потрібно знати два сусідні вузли для того, щоб пройти або змінити список. Ви торгуєте простором для зручності. Це висвітлено у пов’язаній статті.
Крейг Гідні,

8

На мою думку, найкориснішим є той випадок, який насправді має потенціал зробити програми набагато ефективнішими: ряд стандартних та загальних інтерфейсів бібліотеки приймають один void *аргумент, який вони передають назад до певної функції зворотного виклику. Припустимо, для вашого зворотного виклику не потрібен великий обсяг даних, а лише один цілий аргумент.

Якщо зворотний виклик відбудеться до повернення функції, ви можете просто передати адресу локальної (автоматичної) intзмінної, і все добре. Але найкращим реальним прикладом для цієї ситуації є pthread_create, коли "зворотний виклик" працює паралельно, і ви не маєте гарантії, що він зможе прочитати аргумент через покажчик до pthread_createповернення. У цій ситуації у вас є 3 варіанти:

  1. mallocсингл, intа новий потік прочитайте і freeце.
  2. Передайте вказівник на локальну структуру, що викликає, що містить intоб'єкт синхронізації та (наприклад, семафор або бар'єр), і нехай абонент чекає на нього після виклику pthread_create.
  3. Закиньте intдо void *і передати його за значенням.

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


2
І щоб думати, що це може бути зроблено на 100% безпечним, розробивши ці функції, візьміть union {int i; void* p;}замість a void*.
Кос

2
Більш безпечний, але набагато прикріший у використанні. Pre-C99 (тобто без складених літералів), передаючи unionнеобхідну, роблячи потворну змінну температури. Інтерфейси сигналів POSIX у реальному часі використовували цей підхід ( union sigval), і всі його ненавидять ...
R .. GitHub СТОП ДОПОМОГАЙ ЛЕД

8

Одним з прикладів є Windows, наприклад, функції SendMessage()and PostMessage(). Вони беруть a HWnd(дескриптор до вікна), повідомлення (інтегральний тип) і два параметри для повідомлення, a WPARAMта an LPARAM. Обидва типи параметрів є цілісними, але іноді потрібно передавати покажчики, залежно від надісланого повідомлення. Тоді вам доведеться накинути вказівник на LPARAMабо WPARAM.

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


1
Це насправді не використання цього, це просто тому, що вони є застарілим кодом і такий дизайн був загальним. У більш сучасній системі ви просто надасте кілька зворотних викликів.
Щеня

Я не роблю WinAPI, тому не знав, що люди це роблять. Ви знаєте, чи WinARI гарантує LPARAM та WPARAM достатньо великі розміри, щоб мати змогу поєднати покажчик?
Кос

Концептуально LPARAMце не інтегральний тип, а LONG_PTR- об'єднання вказівника та інтегральний тип. Але це справді трохи хакерства. @DeadMG: Можна, SendMessageзбоку. Але проблема залишається GetMessage. Ви не можете перевантажити це, тому що не можете передбачити, яке повідомлення ви отримаєте.
MSalters

@MSalters: Сьогодні це може бути LONG_PTR, кілька років тому він все ще був інтегральним типом (UINT або DWORD, IIRC). Вам все одно довелося використовувати їх для передачі покажчиків. @ DeadMG: це коли ти робиш.
Rudy Velthuis

1
@Kos: так, вони гарантовано будуть досить великими. Інакше Windows сильно заважав би тому, що люди не могли надсилати повідомлення зі значеннями покажчика. Windows використовує повідомлення майже для всього графічного інтерфейсу.
Rudy Velthuis

6

У вбудованих системах дуже часто можна отримати доступ до апаратних пристроїв, відображених у пам'яті, де регістри знаходяться за фіксованими адресами на карті пам'яті. Я часто моделюю апаратне забезпечення по-різному в 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

Ніколи не є корисним виконувати подібні закиди, якщо ви не знаєте повною мірою поведінки комбінації комбінація + комбінація платформи і не бажаєте використовувати її (сценарій вашого запитання є одним із таких прикладів).

Причина, з якої я кажу, що це ніколи не корисно, полягає в тому, що загалом у вас немає контролю над компілятором, ані повного знання того, які оптимізації він може вибрати. Або по-іншому, ви не можете точно контролювати машинний код, який він буде генерувати. Отже, загалом, ви не можете безпечно реалізувати такий фокус.


1
Ви не можете реалізувати це в портативному режимі, але на певній архітектурі / компіляторі ви можете напевно безпечно реалізувати його, якщо розумієте подробиці.
phkahler

Вам не потрібні енциклопедичні знання щодо оптимізації вашого компілятора. Якщо ви хочете довести, що правильно використовуєте зліпки, вам просто потрібно знати кілька інваріантів. Наприклад, на всіх широко використовуваних реалізаціях malloc, (uintptr_t) malloc(n) % 4 == 0коли n> 2. Це досить корисно, щоб ви могли робити з ним цікаві речі, і ваш код буде правильним та безпечним на платформах, де передбачуваний інваріант.
Джейсон Орендорф,

3
Я думаю, що C99 гарантує низку таких речей, як: якщо ви приведете покажчик до uintptr_t, а потім приведете той самий покажчик до uintptr_t, отримані цілі значення будуть однаковими. Цього достатньо, щоб такі закиди були корисними для обчислення хеш-кодів. Трохи інваріант проходить довгий шлях.
Джейсон Орендорф

1
@JasonOrendorff: C99 цього не гарантує. Це гарантує, що покажчик-> uintptr_t-> обертання покажчика дасть покажчик, який порівнюється з оригіналом, але при відповідній реалізації з, наприклад, 48-бітними покажчиками та 64-бітним uintptr_t, щось на зразок uintptr_t asUint = (uintptr_t)somePtr;може просто написати 48 бітів а інші 16 бітів залиште довільними значеннями.
supercat

2

Єдиний раз, коли я призначаю a pointer, integerце коли я хочу зберегти покажчик, але єдиним доступним сховищем є ціле число.


3
І чому ви хочете це зробити? У якій ситуації це корисно? Я б просто змінив сховище на покажчик.
Р. Мартіньо Фернандес

Можливо щось на зразок того, де в старих добрих системах зворотного виклику C існує лише порожнеча *, можливо, можуть бути системи зворотного виклику, які мають лише size_t ...
PlasmaHH

Чи завжди size_t достатньо великий для цього?
Flexo

2
@R. Мартіньо Фернандес: коли це не мій код. Всі вони Componentsмають Tagвластивість, яке є цілим числом. Якщо я хочу пов'язати об'єкт / структуру / рядок / покажчик з a Component, я можу зробити це через Tagвластивість.
Ян Бойд

3
Приклад: У "класичному" MacOS багато структур, наприклад, WindowRecordмали 4-байтове userInfoполе, яке можна було використовувати для зберігання будь-якої інформації, яке ви хочете, і яке зазвичай використовувалося для зберігання вказівника на допоміжну структуру. У таких випадках вам доведеться перекинути вказівник на int або long (я не пам’ятаю, який) і назад, просто щоб зробити компілятор щасливим.
Калеб

2

Коли правильно зберігати покажчики в ints? Це правильно, коли ви ставитеся до нього як до того, що воно є: використання специфічної поведінки платформи або компілятора.

Проблема полягає лише в тому, що у вашому додатку є код, специфічний для платформи / компілятора, і вам доведеться перенести свій код на іншу платформу, оскільки ви зробили припущення, які більше не відповідають дійсності. Ізолюючи цей код і приховуючи його за інтерфейсом, який не робить припущень щодо базової платформи, ви усуваєте проблему.

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

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


1

Можливо, вам знадобиться отримати доступ до пам'яті за фіксованою відомою адресою, тоді ваша адреса є цілим числом, і вам потрібно призначити її покажчику. Це дещо поширене у вбудованих системах. І навпаки, вам може знадобитися надрукувати адресу пам’яті і, отже, її потрібно перетворити на ціле число.

О, і не забувайте, що вам потрібно призначити та порівняти покажчики з NULL, що зазвичай є показником 0L


тоді добре. Коли ви пишете бібліотечну процедуру, яка друкує покажчики. Дух!
deStrangis

У C ++ 0 є нульовий покажчик літералу, і жоден приклад не бере участь. Насправді бітовий шаблон нульового вказівника навіть не повинен бути однаковим, як цілого числа того самого розміру зі значенням 0 ...
PlasmaHH

Так, хороший момент, але це не той випадок, коли мова йде про C (зверніть увагу, я зазвичай вживав це слово). Якщо ви маєте справу з фіксованими постійними адресами пам’яті - ситуація, яка мені спадає на думку, коли вам потрібно робити ціле число для перекидання покажчиків - я б сказав, що ви, швидше за все, будете використовувати C, ніж C ++.
deStrangis

І закиди використовуються набагато рідше в C ++, ніж у C у будь-якому випадку, де вони є важливими.
deStrangis

1

Я маю одне застосування для такого в мережевих ідентифікаторах об’єктів. Такий ідентифікатор поєднує в собі ідентифікацію машини (наприклад, IP-адресу), ідентифікатор процесу та адресу об'єкта. Для надсилання через сокет частину покажчика такого ідентифікатора потрібно помістити в досить широке ціле число, щоб воно вижило при транспортуванні туди-сюди. Частина покажчика інтерпретується як покажчик (= відкидається назад до покажчика) лише в тому контексті, коли це має сенс (одна і та ж машина, той самий процес), на інших машинах або в інших процесах, вона просто служить для розрізнення різних об'єктів.

Все, що потрібно мати, що працює, - це існування uintptr_tі uint64_tяк ціле число типу фіксації ширини. (Ну працює лише на машинах, що мають не більше 64 адрес :)


1

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

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

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

1
Хе. Майк Палл не винайшов цієї техніки. Я впевнений, що це бере початок із ранніх реалізацій Lisp.
Джейсон Орендорф

4
AMD спеціально застерігає від цього, оскільки це розірве жахливо, коли розшириться адресний простір. Так само, як це було на 68000, коли адресний простір було розширено з 24 до 32 біт.
Бо Перссон,

Тільки на другий план сказаного Джейсоном, цей прийом справді давній і застосовувався в незліченних мовних середовищах.
Стівен Канон

@Bo: отримав посилання на це? Цікаво, що ще воно може містити. Джейсон: оновлено, щоб відобразити ваш коментар :)
Некроліс,

1
@Eonil: очевидно , що якщо ви збираєтеся робити покажчик мічення або створення системи управління пам'яттю, ви повинні були б знати вашу базову архітектуру, моя відповідь орієнтуючись в основному на x86, так і під x86_64, все адресний простір є лінійним, так що це гарантовано :)
Некроліс,

0

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

Наприклад, покажчики int:

int* my_pointer;

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

Це насправді єдиний спосіб досягти цього, крім перекидання вказівника на (char *). ((char*)my_pointer)++

Слід визнати, що (char *) - це мій звичний метод, оскільки він має більше сенсу.


(char*)це єдиний метод, який гарантовано буде чітко визначений.
GManNickG

0

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

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

Бібліотека boost UUID використовує цей трюк та деякі інші.


Не гарантується, що в наступних прогонах new int()(до речі, ініціалізація не потрібна) виробляє інше значення. Існують чітко визначені джерела ентропії, такі як/dev/random
Максим Єгорушкін

0

Існує давня і добра традиція використовувати вказівник на об’єкт як безтиповий дескриптор. Наприклад, деякі люди використовують його для реалізації взаємодії між двома блоками C ++ з плоским API у стилі C. У цьому випадку тип дескриптора визначається як один із цілочисельних типів, і будь-який метод повинен перетворити покажчик у ціле число, перш ніж його можна буде передати в інший метод, який очікує абстрактний безтиповий дескриптор як один із своїх параметрів. Крім того, іноді немає іншого способу розбити кругову залежність.


Я не уявляю такої ситуації ... Чи могли б ви надати зразок коду?
Кос

Це неможливо уявити, оскільки це не абстрактна ситуація. Це дуже конкретний випадок, який важко проілюструвати коротким зразком. Загальне правило: якщо ви можете реалізувати взаємодію без нетипових ручок, не використовуйте їх. Але одного дня ти міг зіткнутися з тим, що іншого шляху не існує. У такому випадку використовуйте його без сумнівів. Це законний спосіб, якщо ви перевіряєте тип об’єктів під час виконання після розпакування покажчиків із цілих чисел (наприклад, за допомогою методу get_type_id ()).
Сергій Шамов

Вони часто використовують об’єднання вказівника та цілого числа. Див struct epoll_dataдля epoll_ctl , наприклад.
Максим Єгорушкін

Профспілки корисні в такій ситуації. Це як кастинг, але набір цільових типів обмежений.
Сергій Шамов
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.