使用void*进行转换而不是使用reinterpret_cast

37

我正在阅读一本书,发现直接使用reinterpret_cast是不可以的,应该将其与static_cast配合使用,先转换为void*类型:

T1 * p1=...
void *pv=p1;
T2 * p2= static_cast<T2*>(pv);

改为:

T1 * p1=...
T2 * p2= reinterpret_cast<T2*>(p1);

然而,我找不到为什么这比直接转换更好的解释。如果有人能给我一个解释或指向答案,我将非常感激。

谢谢提前。

附注:我知道何时使用reinterpret_cast,但从未看到过它以这种方式使用。


2
谢谢您的提问。答案似乎深藏在标准中。(曾经在Usenet上问过几次,没有人能保证这种静态转换序列做得更好)。肯定有些东西:C++0x在reinterpret_cast的规范中添加了一些措辞,当它与某些类型一起使用时,将其重写为static_cast序列。 - Johannes Schaub - litb
@sinec 我必须问一下,您为什么觉得有必要执行这样的转换?多年来,我已经写了大量的C++代码,而没有这样的需求。 - anon
@Neil Butterworth 我曾经参与了一个项目(更像是在现有代码中添加新功能),其中有很多疯狂的转换,但这是不可避免的,因为我们无法改变遗留代码。无论如何,我问了这个问题,因为我正在阅读一本书,但我找不到解释。 - sinek
1
哪本书?无论如何,找一本更好的书。 - curiousguy
4
这本书是《C++ 编码规范》(Sutter/Alexandrescu)- 类型安全部分(第91章)。 - sinek
3个回答

26

对于允许这种转换的类型(例如,如果T1是POD类型且T2unsigned char),使用static_cast的方法符合标准。

另一方面,reinterpret_cast完全由实现定义 - 您所获得的唯一保证是可以将指针类型转换为任何其他指针类型,然后返回原始值;此外,您可以将指针类型强制转换为足以容纳指针值的整数类型(这取决于实现,并且可能根本不存在),然后将其转换回来,您将获得原始值。

更具体地说,我只引用了标准的相关部分,并突出了重要部分:

5.2.10[expr.reinterpret.cast]:

reinterpret_cast执行的映射是实现定义的。[注意:它可能会产生与原始值不同的表示。]…可以将指向对象的指针显式转换为不同类型的对象的指针。)除了将类型为“指向T1的指针”的rvalue转换为类型“指向T2的指针”(其中T1和T2是对象类型,且T2的对齐要求不比T1严格),然后返回其原始类型的指针值,此类指针转换的结果是未指定的

因此,像这样的内容:

struct pod_t { int x; };
pod_t pod;
char* p = reinterpret_cast<char*>(&pod);
memset(p, 0, sizeof pod);

“未指定”在 IT 技术中是一个常用术语。

解释为什么 static_cast 起作用有点棘手。以下是使用 static_cast 重写的上面的代码,我相信标准保证它始终按预期工作:

struct pod_t { int x; };
pod_t pod;
char* p = static_cast<char*>(static_cast<void*>(&pod));
memset(p, 0, sizeof pod);

再次引用标准的章节,综合判断上述内容应该是可移植的:

3.9[basic.types]:

对于任何POD类型T(除了基类子对象),不管这个对象是否持有有效的T类型值,构成该对象的底层字节(1.7)都可以被复制到char或unsigned char数组中。如果将char或unsigned char数组的内容复制回对象中,则对象随后应保持其原始值。

T类型对象的对象表示是由T类型对象占用的N个无符号char 对象序列,其中N等于sizeof(T)。

3.9.2[basic.compound]:

带有cv限定符(3.9.3)或未带cv限定符的类型void*(指向void的指针)可以用于指向未知类型的对象。一个void*应当能够容纳任何对象指针。带有cv限定符或未带cv限定符(3.9.3)的void*应具有与带有cv限定符或未带cv限定符的char*相同的表示和对齐要求。

3.10[basic.lval]:

如果程序试图通过除以下类型之一以外的lvalue访问对象的存储值,则行为未定义:

  • ...
  • char或unsigned char类型

4.10[conv.ptr]:

类型为“指向cv T”的rvalue(其中T是对象类型)可以转换为类型为“指向cv void”的rvalue。将“指向cv T”的指针转换为“指向cv void”的结果指向对象T所在的存储位置的开始,就像对象是类型T的最派生对象(1.8),而不是基类子对象。

5.2.9[expr.static.cast]:

除了lvalue-to-rvalue(4.1),array-topointer(4.2),function-to-pointer(4.3)和boolean(4.12)转换之外的任何标准转换序列(第4章),都可以使用static_cast进行显式转换的逆操作。

[编辑] 另一方面,我们还有这个宝石:

9.2[class.mem]/17:

使用reinterpret_cast转换的POD结构体对象指针,指向其初始成员(如果该成员是位域,则指向其所在单元),反之亦然。[注意:因此,在POD结构体对象内可能存在未命名的填充,但不会在其开头处出现,因为这是为了实现适当的对齐。]
这似乎意味着指针之间的reinterpret_cast转换暗示着“相同的地址”。自己琢磨吧。

1
但是对于static_cast的结果,不能保证将其转换为void*并返回到不同类型。它只是说:“将对象指针类型转换为“cv void指针”并返回到原始指针类型的值将具有其原始值。” - Johannes Schaub - litb
请查看编辑后的答案。虽然没有明确说明“这是可以的”,但标准中有许多引用强烈暗示它是可以的。特别要注意的是,任何POD都由char _对象_组成,将POD结构指针static_cast为void*会产生一个指向第一个这样的char对象的void*指针(因为在POD结构中不允许有初始填充)。 - Pavel Minaev
如果我们以某种方式单独获得了指向对象表示中第一个字符的指针(没有使用static_cast),并将其转换为void*,那么标准间接要求它等于指向对象本身转换为void*的指针。由此可见,正如我们可以将前面的指针向后转换为char*并使其工作一样,我们也可以将后面的指针"向后转换"为char*并使其工作(因为它是相同的指针值!)。 - Pavel Minaev
4
然而,请注意最近的编辑。这个表述令人费解,我倾向于现在说,在我之前的评论中提到的逻辑下,static_castreinterpret_cast 实际上是保证有效的。 - Pavel Minaev
-1 "标准中有许多参考资料强烈暗示它是这样的。 " 所有这些参考资料甚至都没有提到 static_cast - curiousguy

6

毫无疑问,意图是两种形式都被明确定义了,但措辞未能恰当表达。

实际上这两种形式都能正常工作。

reinterpret_cast 更明确地表达了意图,应优先选择使用。


4
这样做的真正原因是由于C++如何定义继承以及成员指针。
在C中,指针基本上只是一个地址,这是应该的。在C++中,它必须更加复杂,因为它具有一些特性。
成员指针实际上是类中的偏移量,因此使用C风格进行转换总是会造成灾难。
如果您有多重继承了两个虚拟对象,这些对象还具有一些具体部分,在C风格下也会造成灾难。这就是多继承引起所有问题的情况,因此您根本不应该想要使用它。
希望您从一开始就永远不要使用这些情况。另外,如果您经常进行转换,那么这是您设计中出了问题的另一个标志。
我最终进行转换的唯一时间是在C++认为它们不相同但明显必须相同的区域中的基元时。对于实际对象,每当您想要转换某些内容时,请开始质疑您的设计,因为大多数情况下应该“编程到接口”。当然,您无法更改第三方API的工作方式,因此您并不总是有太多选择。

我同意你在这里说的大部分内容,但是对于强制类型转换的负面评价可能有点过了。强制类型转换有其应用场景(就像大多数其他语言特性一样),根据我最近所做的一些工作,一些有用的例子包括:在非const运算符重载中使用const_cast调用const重载(其中逻辑相同),调用转换运算符以防止代码重复,解决歧义(输出小整数字面值与字符字面值),以及使用重载地址运算符获取对象的真实地址。我想每个特性都有其应用时机吧。 - monkey0506

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