如果 `uintptr_t` 可以用来存储指针和数字,为什么 `void*` 不能做同样的事情?

If a `uintptr_t` can be used to store both pointers and numbers, why can't a `void*` do the same?

枚举等语言Swift 或 Rust 支持一种混合的“选择 plus 数据”机制,这样我就可以定义如下类型:

enum SomeOption {
  None,
  Index(int),
  Key(string),
  Callback(fn),
}

现在如果我在 C 中实现它,我的理解是这样的东西 不是 是有效的:

typedef enum {
  is_callback_or_none,
  is_string,
  is_number
} my_choice;

typedef struct {
   my_choice value_type;
   void* value;
} my_option;

my_option x = {
  .value_type = is_number,
  .value = (void*)42
};
if (x.value_type == is_number) {
  int n = (int)x.value;
  // … use n…
}

我不确定这样做到底有什么风险,但是根据例如Can pointers store values and what is the use of void pointers? the only things I should store in a void* are actual addresses and NULL. [Aside: please turn a blind eye to the separate question of storing callback function pointers in a void* which I forgot was 这个例子是我编出来的。]

我想更合适的方法是使用联合,例如:

typedef struct {
   my_choice value_type;
   union {
      int number_value;
      char* string_value;
      void* pointer_value;
   };
} my_option;

…无论如何,这可能更好。 但是我特别想知道 void* value 版本的无效性。如果我(而不是 union 解决方案)简单地用 uintptr_t 代替 void* 怎么办?

typedef struct {
   my_choice value_type;
   uintptr_t value;
} my_option;

将存储 指向 string/callback/null uintptr_t value 字段中的数字的指针结构有效且 [至少 POSIX-] 可移植代码?如果是这样,为什么可以,但看似等效的 void* value 版本却不行?

The problem is that I don't understand if the rules are different re. what I can do with a uintptr_t/intptr_t vs. a void*, and if so, why they would be different?

规则是不同的,因为它们处于边缘,在机器实际可以做什么、人们想做什么以及语言标准规定它们可以做什么之间的边界(或灰色区域)内.

现在,是的,在“传统”体系结构中,指针和整数都只是具有一定大小的二进制整数,因此显然可以在两者之间混合“n”匹配。

而且,是的,这显然是某些人发现自己想要做的事情。你有一大堆不同种类的东西,其中一些是普通数字,一些是数据指针,也许其中一些是函数指针,你有一些方法可以知道哪个是哪个,有时将它们存储在一个大的异构数组中确实看起来很整洁。或者你有一个带有参数的函数,它有时想成为一个整数,有时想成为一个指针,你也可以接受。 (好吧,除了你从编译器那里得到的所有警告,以及语言律师和 SO 常客的讲座。)

但是还有 C 标准,它在区分整数、数据指针和函数指针方面付出了一些努力。在某些架构中,这些确实不可互换,并且尝试是个坏主意。 C 标准一直试图适应这些架构。 (如果你不相信我,尽管问,因为例子确实存在。)

C 标准可以 说所有指针和所有整数都可以更自由地互换。听起来这就是 Swift 和 Rust 所做的。但这也听起来 Swift 并且 Rust 无法在那些假设的“异国情调”架构上实现。

这些讨论变得棘手,因为它们也处于语言标准和编程实践之间的交叉点。如果你知道你总是要使用整数和指针可以互换的机器,如果你不关心对其他“奇特”架构的可移植性,你可以这么说,忽略警告,继续前进-- 除非你的内部风格指南说禁止转换,或者你的软件开发计划说你的代码必须严格符合并且编译时没有警告。然后你可能会发现自己在争论或试图改变 C 标准,只是为了绕过你自己的 SDP。 (我的建议:确保你的风格指南和你的 SDP 有豁免机制。)

总有一天,C 标准可能会减少对那些异国情调架构的“保护”(启用?)。例如,我听说有人在争论是否要放弃补码和 sign/magnitude 机器的适应性,并将补码的定义构建到 C 标准的下一个修订版中。但是,如果发生这种情况,或者如果其他 guarantees/accommodations 发生变化,这并不意味着不能再为奇异机器编写 C 编译器和 C 程序——它只是意味着那些机器的程序员将拥有应用他们自己的规则(例如,“不要在 intvoid *void (*)() 之间分配”),这些规则实际上不再存在于标准中。 (或者,等价地,这意味着为“正常”架构编写的严格符合代码不会自动移植到异国情调的架构。另外,我猜,异国情调架构的编译器供应商将无法声明标准不再合规。)

即使 void * 值表示为数字,也不意味着编译器会像处理数字一样处理它们。

一个uintptr_t是一个数字; C 2018 7.20.1.4 1 说它指定了一个无符号整数类型。因此它的行为类似于其他无符号整数类型:您可以将任何数字放入其范围内并返回相同的数字(并且您可以对其进行算术运算)。该段进一步说任何有效的 void * 都可以转换为 uintptr_t 并且将其转换回来将产生原始指针(或等效的东西,例如指向相同位置但具有不同表示的指针)。所以你可以在 uintptr_t 对象中存储指针。

但是,C 标准并没有说明可以将数字范围放入 void * 并取回它们。 6.3.2.3 5 说当整数转换为指针类型时,结果是实现定义的(除了将常量零转换为 void * 会产生空指针,根据 6.3.2.3 3)。 6.3.2.3 6 表示将指针转换为整数时,结果是实现定义的。 (当数字是最初来自指针的 uintptr_t 时,7.20.1.4 会覆盖它。)

那么,如果您将一个数字存储在 void * 中,您怎么知道它会起作用? C 标准不向您保证它会工作。您需要编译器的一些说明它可以工作的文档。