将指向类型化/大小枚举的指针转换为指向基础类型的指针是否安全?

13

以下代码:

void f(const uint8_t* a) {}  // <- this is an external library function
enum E : uint8_t { X, Y, Z };

int main(void) {
  E e = X;
  f(&e);  // <- error here
}

会产生以下错误:

/tmp/c.cc:10:3: error: no matching function for call to 'f'
  f(&e);
  ^
/tmp/c.cc:5:6: note: candidate function not viable: no known conversion from 'E *' to 'const uint8_t *' (aka 'const unsigned char *') for 1st argument
void f(const uint8_t* e) { }

这让我感到惊讶,因为我认为枚举定义中的 : uint8_t 意味着它们必须用该基础类型表示。但我可以通过转换轻松解决这个问题:

f((uint8_t*)&e);

我不太介意这点,但考虑到省略它是一个错误,这样做总是安全的吗?或者: uint8_t并没有提供我认为它提供的保证?


1
你会希望这是一个错误。如果问题中的函数分配了超出您的“enum”范围的值,你想要它就这样发生吗?这就像是说允许将“int *”传递给期望“unsigned *”的函数而不进行强制转换是个好主意;当然,它们通常具有相同的大小,但即使如此,其中一半范围对另一半来说完全具有不同的含义。 - ShadowRanger
你可能会在强制转换时违反了严格别名规则。 - Jarod42
e 作为 uint8_t 存在于内存中,所以强制转换应该是安全的。 - Jacek
除了它是一个相当糟糕的设计,这是一个有趣的问题。请注意 OP 没有使用该值。结果是未指定的(自 C++17 起是 UB),仅当从基础类型转换为枚举值时,其在枚举类型中没有覆盖范围的值。而 OP 甚至没有强制转换枚举类型,而是地址。 - luk32
已更新问题以反映实际用例,我认为这不是糟糕的设计,也不会受到@ShadowRanger提到的问题的影响。 - Luis
2个回答

7
确实是安全的(虽然我不是语言专家):存储在内存中的是uint8_t,这就是您将要指向的内容。但是,如果f()接受一个指向非const uint8_t的指针,则它可能会将值更改为未明确定义为E枚举值之一的某些值。(编辑:)尽管C++标准显然允许此操作,但对于许多人来说,这很令人惊讶(请参见下面关于此点的评论讨论),我建议您确保不会发生这种情况。
...但正如其他人所建议的那样,您之所以出现错误并不是因为您的安全概念,而是因为指向类型之间不执行隐式转换。您可以将E传递给接受uint8_t的函数,但不能将E *传递给接受uint8_t *的函数;根据语言委员会和我的意见,这将是过于鲁莽的指针类型态度。

1
在具有显式基础类型的枚举中,基础类型的所有值是否也都有效于枚举?如果是,则它可能会将该值更改为不是有效“E”值的内容。 - user743382
1
@hvd:我不这么认为。enum E 的唯一有效元素是在其中明确定义的元素,据我所知,指定底层类型不会影响它。 - einpoklum
1
@hvd:真的吗?我本来不会猜到那是这样的。你确定那句话中倒数第二个“the”不是误打误撞加进去的吗? - einpoklum
如果编译器根据语言的别名规则执行优化,则内存中的表示实际上并不重要。对于具有相同内存表示的作用域枚举,我确实遇到过g ++优化器执行此操作的情况。许多编译器之所以认为普通枚举与底层类型不同,我认为是出于历史原因和C语言较少的限制。 - MikeMB
@einpoklum:枚举变量绝对可以假定存在没有常量的值。例如,gsl::byte被定义为一个作用域枚举,没有任何元素。 - MikeMB
显示剩余7条评论

6
据我所知,这只是偶然合法的操作:
你正在执行一次reinterpret_cast操作,我假设f在内部对该指针进行了解引用。这只在非常有限的情况下才是合法的,虽然不是规范,但cppreference.com给出了这些情况的概述:
当一个指向动态类型为DynamicType的对象的指针或引用被reinterpret_cast(或C风格转换)为指向不同类型AliasedType的对象的指针或引用时,转换总是成功的,但是得到的指针或引用只有在以下情况之一成立时才能用于访问对象:
- AliasedType是(可能带有cv限定符的)DynamicType。 - AliasedType和DynamicType都是指向相同类型T的(可能多级、可能在每个级别上带有cv限定符的)指针(自C++11以来)。 - AliasedType是DynamicType的(可能带有cv限定符的)signed或unsigned变体。 - AliasedType是聚合类型或联合类型,其中包含上述类型之一作为元素或非静态成员(包括递归地包含子聚合的元素和包含联合的非静态数据成员):这使得通过指向其非静态成员或元素的指针可以安全地获得对结构体或联合体的可用指针。 - AliasedType是DynamicType的(可能带有cv限定符的)基类,而DynamicType是具有没有非静态数据成员的标准布局类,并且AliasedType是其第一个基类。 - AliasedType是char、unsigned char或std::byte:这允许将任何对象的对象表示作为字节数组进行检查。
如果AliasedType不满足这些要求,则通过新指针或引用访问对象会调用未定义的行为。这被称为严格别名规则,适用于C++和C编程语言。
这些情况都不包括将指针转换为枚举底层类型的情况!
然而:
将其强制转换为unsigned char*并进行解引用始终是合法的,在大多数平台上,uint8_t只是该类型的typedef。因此,在这种情况下是可以的,但如果底层类型是uint16_t,就不行了。
话虽如此,我不会感到惊讶,如果大多数编译器允许这样的使用,即使标准不允许。

实际上,std::byte 被定义为 enum class byte : unsigned char {};,并且根据最后一点,reinterpret_cast 是合法的。 - Giovanni Cerretani
1
我不认为这很相关。最后一点说您可以将指针转换为std::byte - 而不是std::uint8_t,后者是一个独立的类型。 - MikeMB

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