为什么在这里使用void**?

9

v8-0.2.5中提取的代码

/**
 * Checks whether two handles are the same.
 * Returns true if both are empty, or if the objects
 * to which they refer are identical.
 * The handles' references are not checked.
 */
template <class S> bool operator==(Handle<S> that) {
  void** a = reinterpret_cast<void**>(**this);
  void** b = reinterpret_cast<void**>(*that);
  if (a == 0) return b == 0;
  if (b == 0) return false;
  return *a == *b;
}  

Handle 重载运算符*,使得 **this*that 返回类型为 T*

看起来是这样的:

  void* a = reinterpret_cast<void*>(**this);
  void* b = reinterpret_cast<void*>(*that);
  return a == b;

也能正常工作吗?


1
这种魔法以前在旧的C代码中完成,用于传递void*类型的引用变量。我认为,如果你将返回值改为a == b,并且将b和a都定义为void*类型,它将能够很好地工作。 - Endzior
如果对Handle<T>进行解引用会产生一个T*,那么这段代码就存在严重的缺陷,因为比较会产生假阳性和假阴性:如果sizeof(T) < sizeof(void*),则会出现假阴性,因为比较中包含了不属于对象的内容;如果sizeof(T) > sizeof(void*),则会出现假阳性,因为只有对象的前四/八个字节被比较。 - cmaster - reinstate monica
3个回答

2
如果ab的类型是void*,那么你不能直接引用它们(除非先将它们转换为其他类型),因此*a == *b不起作用。请注意保留HTML标签。

但是你难道不能只是 a == b 吗? - Endzior
2
不行,因为在文档注释中它说“对象是相同的”,这意味着它们不必是完全相同的对象才能返回true,只要它们是相同的即可,即使是两个不同的对象也可以比较相等。 - Tomasz Plaskota

2
首先,我承认我只是粗略地阅读了链接中的代码。显然,Handle类重载了解引用运算符(*),以返回正在处理的T*。因此,第一行中的表达式意味着以下内容:
  • this 是一个(可能带有cv限定符的)Handle<T> * const
  • *thisHandle<T> &
  • **this 是句柄的operator*的返回值,即您提到的T*
  • 最后,该T*被重新解释为void**。请注意,添加了额外的间接性,因此可以对结果进行解引用,并将得到void* &而不是T&
  • 使用that的等效行产生一个S*,它被重新解释为void**

因此,您得到了几个指向不同类型T*S*的指针,这些指针神奇地被重新解释为void**。然后,代码执行空指针检查,然后是神奇的那一行:

return *a == *b;

比较两个类型为T和S的对象,这些对象由a和b指向,并且可能不对齐。比较的是这些对象的前sizeof(void*)个字节。如果你无法确定T和S的大小和对齐方式,则此检查完全无效。例如,如果您知道T本身始终是具有与指针相同大小的指针或智能指针对象,则该检查是有意义的,因此您可以使不同的句柄指向不同的指针对象,这些指针对象(在第二个间接级别上)指向同一个对象。这允许GC移动基础对象而无需更新所有句柄,只需更新第一级指针的内容即可。请注意保留HTML标记。
Handle<T> has T* -----> T = U* pinned in memory -----> actual object U can be moved

因此,回答你的问题,仅将类型转换为void*(不增加间接性)与在代码中进行检查并不相同 - 你的版本将比较指针,所以在我的示例中,两个指向同一对象的不同句柄可能会与你的替代代码比较不相等。
PS:从operator*operator->中同时返回T*是不好的风格,因为这样你就破坏了p->x(*p).x之间的普遍关系。解引用运算符通常应该返回T&,如果成员访问运算符返回T*

在查看了V8代码后,我认为这是一个正确的分析。依我之见,根据所处理的类型,所呈现的代码具有未定义的行为,因为任意类型对象都被作为指针访问(这是未定义的原因是某些体系结构在其地址寄存器中加载无效位模式时可能会陷入)。正确的方法是将T*值转换为void *,然后进行数值比较;然后在将两个T*都转换为char指针后,可以比较TS的字节,这是唯一允许的例外情况。 - Peter - Reinstate Monica
字节级比较是可能的,因为对于模板,sizeof(T)sizeof(S)是已知的(但在示例中被忽略;实际上,如果底层类型比地址小(潜在的8个字节!),则按原样进行比较会比较不相关内存的部分,逻辑上也是错误的)。 - Peter - Reinstate Monica

0

Javier Martín 是正确的。你不能像你在问题中建议的那样仅仅比较指针。首先,在 v8 的上下文中,Handle<T> 对类型 T 有限制。你不能将任何类都应用到 Handle 上。 T 只是一个外观类,用户处理它:v8::Stringv8::Integer 等等。这些类型的对象从未被创建,但是这些类被用作内部接口。

实际上,Handle<> 存储的指针是指向某个东西(比如“标签”)的指针。如果两个 Handle<> 的标签相同,则引用相同的对象。标签在内部具有 void* 的大小,并且以某种方式引用真正的对象。用户不需要知道 Tag 是什么,因此 Handle<> 使用 void*。一些想法

  1. Handle<T>T* --> 标签(不是 T)--(以某种方式)--> 真实对象(不是 T 类型)

  2. 标签的大小为 void*。用户不需要知道标签的真实类型。

  3. 两个标签相等 - 它们引用同一个对象。

  4. (从用户的角度总结) Handle<T>void** --> void*

因此,原始的 bool operator==(Handle<S> that) 做了它应该做的事情:比较标签的值。但首先检查指针。


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