可携式标记指针

8
有没有一种便携式的方法在C/C++中实现标记指针,比如一些文档化的宏,可以跨平台和编译器工作?或者当你标记你的指针时,你就处于自己的危险之中了吗?如果这样的辅助函数/宏存在,它们是标准的一部分还是只作为开源库可用?
对于那些不知道什么是标记指针但感兴趣的人,它是一种在普通指针内存储一些额外数据的方法,因为在大多数架构上,指针中的一些位始终为0或1,所以你将你的标志/类型/提示保存在这些额外位中,并在你想要使用指针解引用某个实际值之前将其擦除。
const int gc_flag = 1;
const int flag_mask = 7; // aka 0b00000000000111, because on some theoretical CPU under some arbitrary OS compiled with some random compiler and using some particular malloc last three bits are always zero in pointers.

struct value {
   void *data;
};

struct value val;
val.data = &data | gc_flag;
int data = *(int*)(val.data & flag_mask);

https://en.wikipedia.org/wiki/Pointer_tagging


因为在 x86_64 上这样做不具有可移植性。 - kmdreko
3
@vu1p3n0x,你说得对,因此我的问题是,是否有可能使它可以携带。 - exebook
1
3 不等于 0b111。 - Eljay
6
不,这个无法做到便携。早些年,基于68000的机器具有32位指针,其中高8位是闲置的。因此,开发人员会使用这些位来存储元数据。这很好... 直到68020和68030问世,它们使用了所有位。这导致了很多麻烦(或者说,这也为工作安全带来了保障,这取决于你的观点)。 - Eljay
1
这取决于你所说的“可移植”的含义。有没有一种方法可以在Windows、Mac和Linux上运行,适用于所有常见处理器类型,至少对于具有足够大的对齐要求的类型?是的,而且对于这些系统,它甚至不会调用未定义的行为,因为实现有设计文档来参考其ABI。是否有一种方式可以完全符合C++标准并且可以在任何地方工作?没有。 - Daniel H
显示剩余5条评论
3个回答

5

您可以通过确保对象对1 << N的倍数进行对齐,来获取地址的最低N位以供个人使用。这可以通过不同的方式(alignasaligned_storage适用于基于堆栈的对象,std::aligned_alloc适用于动态对象)在平台上实现,具体取决于您想要实现什么:

struct Data { ... };

alignas(1 << 4) Data d; // 4-bits, 16-byte alignment
assert(reinterpret_cast<std::uintptr_t>(&d) % 16 == 0);

// dynamic (preferably with a unique_ptr or alike)
void* ptr = std::aligned_alloc(1 << 4, sizeof(Data));
auto obj = new (ptr) Data;
...
obj->~Data();
std::free(ptr);

你需要花费大量的内存来支付,其量随所需位数呈指数增长。此外,如果你计划连续分配许多这样的对象,对于相对较小的数组,这样的数组将无法适应处理器的缓存行,可能会大大减慢程序的速度。因此,这种解决方案不适用于规模化


1
只是不要尝试进行任何指针算术或使用这些对象的数组。鉴于由对齐限制引起的开销,使用普通指针和结构中的标志字段可能更加内存高效。也许可以使用 struct value { void *data, uint16_t flags }; - Andrew Henle
2
@AndrewHenle “在结构体中使用普通指针和标志字段可能更节省内存。” 是的,但如果您处理ABA或无锁算法中的内存回收问题等,则可能不是意图。 - Jodocus

3
如果你确定传递的地址总是有一些位未使用,那么可以使用uintptr_t作为传输类型。这是一个整数类型,以预期的方式映射到指针(并且将无法在提供没有这种可能映射的模糊平台上存在)。
没有标准的宏,但你可以很容易地自己编写。代码(不包括宏)可能看起来像:
void T_func(uintptr_t t)
{
    uint8_t tag = (t & 7);
    T *ptr = (T *)(t & ~(uintptr_t)7);

    // ...
}

int main()
{
    T *ptr = new T;
    assert( ((uintptr_t)ptr % 8) == 0 );
    T_func( (uintptr_t)ptr + 3 );
}

这可能会破坏编译器跟踪指针使用的优化。

1
如果有 void *add_tag(void *p, uint8_t tag);void* split_components(void *tagged_ptr, uint8_t *tag_out); 的原型,这将更有用,但方法是可靠的。 - lockcmpxchg8b
1
@lockcmpxchg8b 我建议使用 uintptr_t 而不是 void *。后者会导致分配小于 8 字节等情况下的未定义行为。 - M.M
我建议将对 uintptr_t 的转换隐藏在函数内部,这样用户就不必在代码中到处添加转换。 - lockcmpxchg8b
通过使用uintptr_t add_tag(void *p, uint8_t tag)void *split(uintptr_t tagged, uint8_t *tag_out),用户可以避免在代码中频繁使用强制类型转换,从而实现对@lockcmpxchg8b的操作。 - M.M
同意。你的意图并不明显。 - lockcmpxchg8b

1
好的,至少GCC可以计算位域的大小,因此您可以在不同平台之间实现可移植性(我没有可用于测试的MSVC)。您可以使用此功能将指针和标记打包到intptr_t中,intptr_t保证能够容纳指针。
#include <limits.h>
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <inttypes.h>

struct tagged_ptr
{
  intptr_t ptr : (sizeof(intptr_t)*CHAR_BIT-3);
  intptr_t tag : 3;
};

int main(int argc, char *argv[])
{
  struct tagged_ptr p;

  p.tag = 3;
  p.ptr = (intptr_t)argv[0];

  printf("sizeof(p):              %zu <---WTF MinGW!\n", sizeof p);
  printf("sizeof(p):              %lu\n", (unsigned long int)sizeof p);
  printf("sizeof(void *):         %u\n", (unsigned int)sizeof (void *));
  printf("argv[0]:                %p\n", argv[0]);
  printf("p.tag:                  %" PRIxPTR "\n", p.tag);
  printf("p.ptr:                  %" PRIxPTR "\n", p.ptr);
  printf("(void *)*(intptr_t*)&p: %p\n", (void *)*(intptr_t *)&p);
}

给出:
$ ./tag.exe
sizeof(p):              zu <---WTF MinGW!
sizeof(p):              8
sizeof(void *):         8
argv[0]:                00000000007613B0
p.tag:                  3
p.ptr:                  7613b0
(void *)*(intptr_t*)&p: 60000000007613B0

我已将标签放在顶部,但更改结构的顺序会将其放在底部。然后将要存储的指针向右移3位即可实现OP的用例。可能需要创建访问宏以使其更易于使用。
我也喜欢这个结构,因为您无法意外地将其解引用为普通指针。

有趣,但所述目标是可移植性。我认为在C语言中,没有比位域更不可移植的东西了。 - Andrew Henle
是的...这可能会使像M.M这样的基于函数的方法成为正确的选择。 - lockcmpxchg8b
1
mingw使用MS的旧CRT库,该库仅支持C89和MS扩展。改用mingw64,并#define __USE_MINGW_ANSI_STDIO 1以使printf与C99格式说明符配合使用。如何在mingw-w64 gcc 7.1中无警告地printf一个size_t?MinGW的msvcrt替代品?(例如获取符合规范的snprintf) - phuclv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接