整数<>指针强制转换何时真正正确?

When is an integer<->pointer cast actually correct?

本文关键字:何时真 转换 lt gt 指针 整数      更新时间:2023-10-16

民间传说说:

  • 类型系统的存在是有原因的。整数和指针是不同的类型,在大多数情况下,在它们之间进行转换是一种弊端,可能表示设计错误,应避免使用。

  • 即使执行这样的强制转换,也不应假设整数和指针的大小(将void*转换为int是使代码在 x64 上失败的最简单方法(,而不是int应该使用 stdint.h 中的 intptr_tuintptr_t 而不是。

知道这一点,什么时候进行这样的演员表才真正有用

(注意:为了便于移植,使用更短的代码并不算作"实际有用"。


我知道的一个案例:

  • 一些无锁多处理器算法利用了 2+ 字节对齐指针具有一些冗余的事实。例如,然后他们使用指针的最低位作为布尔标志。对于具有适当指令集的处理器,这可以消除对锁定机制的需求(如果指针和布尔标志是分开的,则锁定机制是必需的(。
    (注意:这种做法甚至可以通过java.util.concurrent.atomic.AtomicMarkableReference在Java中安全地完成(

还有什么事吗?

当整数以某种方式需要成为哈希和的一部分时,我有时会将指针投射到整数。此外,我将它们转换为整数,以在某些实现中对它们进行一些位拨弄,其中保证指针始终剩下一个或两个备用位,我可以在左/右指针中编码 AVL 或 RB 树信息,而不是有额外的成员。但这一切都是如此特定于实现,我建议永远不要将其视为任何类型的通用解决方案。我还听说有时危险指针可以用这样的东西来实现。

在某些情况下,我需要每个对象都有一个唯一的 ID,我将其传递给服务器等作为我的请求 ID。 根据上下文,当我需要保存一些内存时,这是值得的,我使用我的对象的地址作为这样的id,并且通常必须将其转换为整数。

当使用嵌入式系统(例如在佳能相机中,请参阅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,但它说明了这一点(

使用一半的空间存储双向链表

异或链表将下一个和上一个指针组合成一个大小相同的值。它通过将两个指针异或地放在一起来实现这一点,这需要将它们视为整数。

在我看来,最有用的情况是实际上有可能使程序更有效率的情况:许多标准和通用库接口采用单个void *参数,它们将传递回某种回调函数。假设您的回调不需要任何大量数据,只需要一个整数参数。

如果回调将在函数返回之前发生,您可以简单地传递局部(自动(int变量的地址,一切都很好。但是这种情况最好的实际示例是pthread_create,其中"回调"并行运行,并且您不能保证它能够在pthread_create返回之前通过指针读取参数。在这种情况下,您有 3 个选项:

  1. malloc单个int,并读取并free新线程。
  2. 将指针传递给包含int和同步对象(例如信号量或屏障(的调用方本地结构,并让调用方在调用pthread_create后等待它。
  3. int强制转换为void *并按值传递它。
选项

3 比其他任何一种选择都更有效,这两种选择都涉及额外的同步步骤(对于选项 1,同步是 malloc/free ,并且几乎肯定会涉及一些成本,因为分配和释放线程是不一样的(。

一个例子

是在Windows中,例如SendMessage()PostMessage()函数。它们采用一个HWnd(窗口的句柄(、一条消息(一个整数类型(和消息的两个参数,一个WPARAM和一个LPARAM。这两种参数类型都是积分的,但有时必须传递指针,具体取决于您发送的消息。然后,您必须将指针投射到LPARAMWPARAM

我通常会像瘟疫一样避免它。如果需要存储指针,请使用指针类型(如果可能(。

在嵌入式系统中,访问内存映射硬件设备(其中寄存器位于内存映射中的固定地址(是很常见的。 我经常在 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 种变体,其优缺点可以永远争论,但这里的重点只是说明将整数转换为指针的常见用法。 请注意,此代码不可移植,绑定到特定设备,假设内存区域不是禁区,等等。

执行这样的强制转换是没有用的,除非你完全了解编译器+平台组合的行为,并希望利用它(你的问题场景就是这样一个例子(。

我之所以说它从来没有用,是因为一般来说,你无法控制编译器,也不能完全了解它可能选择做什么优化。 或者换句话说,您无法精确控制它将生成的机器代码。 所以一般来说,你不能安全地实施这种技巧。

我唯一一次将pointer投射到integer时是我想存储指针的时候,但我唯一可用的存储空间是整数。

什么时候在整数中存储指针是正确的?当您将其视为它是什么时,这是正确的:使用平台或编译器特定的行为。

问题只在于,当您在整个应用程序中散落着特定于平台/编译器的代码并且您必须将代码移植到另一个平台时,因为您已经做出了不再成立的假设。通过隔离该代码并将其隐藏在对基础平台不做任何假设的接口后面,可以消除问题。

因此,只要您记录了实现,使用句柄或不依赖于幕后工作方式的东西将其分离到与平台无关的接口后面,然后使代码仅在经过测试和工作的平台/编译器上有条件地编译,那么你就没有理由不使用您遇到的任何类型的巫毒魔法。如果需要,您甚至可以包含大块汇编语言、专有 API 调用和内核系统调用。

也就是说,如果您的"可移植"接口使用整数句柄,整数与某个平台的实现上的指针大小相同,并且该实现在内部使用指针,为什么不简单地将指针用作整数句柄呢?在这种情况下,对整数进行简单强制转换是有意义的,因为您省去了某种句柄/指针查找表的必要性。

您可能需要访问固定已知地址的内存,那么您的地址是一个整数,您需要将其分配给指针。这在嵌入式系统中很常见。相反,您可能需要打印内存地址,因此需要将其转换为整数。

哦,不要忘记你需要分配和比较指向 NULL 的指针,这通常是 0L 的指针转换

我在网络范围的对象的ID中有一个用途。这样的 ID 将结合机器的标识(例如 IP 地址(、进程 ID 和对象的地址。要通过套接字发送,必须将此类 ID 的指针部分放入足够宽的整数中,以便它能够来回传输。指针部分仅在有意义的上下文(同一台机器,同一进程(中被解释为指针(=投射回指针(,在其他机器或其他进程中,它只是用于区分不同的对象。

需要工作的事情是存在uintptr_t并作为固定宽度整数类型uint64_t。(好吧,仅适用于最多有 64 个地址的机器:)

在 x64 下,on 可以使用指针的上位进行标记(因为实际指针仅使用 47 位(。 这对于运行时代码生成之类的事情非常有用(LuaJIT 使用这种技术,根据评论,这是一种古老的技术(,要进行此标记和标记检查,您需要强制转换或union, 这基本上相当于同一件事。

利用分箱的内存管理系统中,将指针转换为整数也非常有用,即:人们可以通过一些数学轻松找到地址的bin/page,这是我不久前写的无锁分配器的示例:

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;

提升 UUID 库使用这个技巧,以及其他一些技巧。

使用指向对象的指针作为无类型句柄有一个古老而良好的传统。例如,有些人使用它来实现两个C++单元之间的交互,具有扁平的 C 样式 API。在这种情况下,句柄类型定义为整数类型之一,任何方法都必须将指针转换为整数,然后才能将其传输到另一个需要抽象无类型句柄作为其参数之一的方法。此外,有时没有其他方法可以分解循环依赖。