`typeid` 代码中奇怪的 `?:` 使用

60
在我正在处理的一个项目中,我看到了这段代码。
struct Base {
  virtual ~Base() { }
};

struct ClassX {
  bool isHoldingDerivedObj() const {
    return typeid(1 ? *m_basePtr : *m_basePtr) == typeid(Derived);
  }
  Base *m_basePtr;
};

我从未见过像那样使用typeid。为什么要与?:一起做这种奇怪的舞蹈,而不是直接执行typeid(*m_basePtr)?可能有什么原因吗?Base是一个多态类(带有虚析构函数)。

编辑:在代码的另一个地方,我看到了这个,并且它似乎是“多余”的等价物。

template<typename T> T &nonnull(T &t) { return t; }

struct ClassY {
  bool isHoldingDerivedObj() const {
    return typeid(nonnull(*m_basePtr)) == typeid(Derived);
  }
  Base *m_basePtr;
};

6
你有没有尝试过不加那个? - Luke
13
可能是遗留问题吗?(也许它并不总是1 ? ... - user166390
4
问题在于条件语句将始终计算为 true,且两个分支返回完全相同的值。你能否查看版本控制的历史记录(如果有)并查看过去是否有所不同? - In silico
1
我同意@pst的观点:很可能是遗留问题。 - NotMe
1
我可能会将其编写为 typeid(*m_basePtr ? *m_basePtr : *m_basePtr) - Nemo
显示剩余6条评论
4个回答

50

我认为这是一种优化typeid的一个鲜为人知而且很少使用(可以说“从未”)的特性是,对于typeid参数的空解引用会抛出异常,而不是通常的UB。

什么?你是认真的吗?你喝醉了吗?

确实。是的。不是。

int *p = 0;
*p; // UB
typeid (*p); // 抛出异常

是的,即使按照C++的丑陋语言标准来衡量,这也很丑陋。

另一方面,在typeid参数内部的任何位置都不起作用,因此添加任何杂乱的内容都将取消此“特性”:

int *p = 0;
typeid(1 ? *p : *p); // UB
typeid(identity(*p)); // UB

声明记录:在此消息中,我并不声称编译器自动检查指针在进行解引用操作之前是否为null必然是一件疯狂的事情。我只是说,在typeid参数的解引用是立即参数时进行此检查,而不是其他地方,完全是疯狂的。(也许这是一些草案中插入的恶作剧,并且从未删除。)

声明记录:在前面的“声明记录”中,我并不声称编译器自动检查指针是否为null并在解引用null时抛出异常(如Java中所示)是有意义的:通常情况下,在null引用上抛出异常是荒谬的。这是一个编程错误,因此异常没有帮助。需要调用断言失败。


5
什么?你是认真的吗?你喝醉了吗? - Mateen Ulhaq
很好的解释。我只希望每当我遇到这个“惯用语”时能记住它;但很可能我不会(我很少看到或使用typeid)。 - Michael Burr
1
标准要求在 typeid 表达式的参数为 lvalue 时抛出异常。在这种情况下任意使用三元运算符并不会改变这种行为,符合标准的编译器 必须 抛出异常。 - David Rodríguez - dribeas
@DavidRodríguez-dribeas 很抱歉我要这么讲究:“*p和(true? *p : *p)的类型完全相同:在两种情况下都是lvalue”,作为l/rvalue不是表达式类型的一部分。我认为没有一个表达式的(type,lvalueness,bitfieldness)名称,这很遗憾。 “在两种情况下都是lvalue”我写了相反的吗?“你可能想重新阅读你的答案”不,但是可能想重新阅读我的上一条评论。“你的评论与之相矛盾(并且与我的评论一致)”完全无稽之谈。你看起来非常累。 - curiousguy
2
“值类别”是指表达式是否为左值等。可能的类别包括{lvalue,xvalue,prvalue}。还有组合{glvalue,rvalue}。 - M.M
显示剩余5条评论

5
我唯一能看到的影响是,1 ? X : X 会给你一个rvalueX,而不是一个普通的lvalueX。这对于像数组(衰减为指针)这样的事情可能很重要,但我认为如果已知 Derived 是一个类,那么这并不重要。也许它是从某个地方复制过来的,其中rvalue-ness很重要?这支持关于“模仿神秘程序”的评论。
关于下面的评论,我进行了测试,确实typeid(array) == typeid(1 ? array : array),所以在某种意义上我错了,但我的误解仍然可以匹配导致原始代码的误解!

10
如果第二个和第三个操作数是左值并且类型相同,结果为该类型的左值。 - ildjarn
2
我认为Visual C++在这方面做错了(需要查看Connect问题报告)。啊,这里有一个条件运算符中(不正确的)右值转换的例子:https://connect.microsoft.com/VisualStudio/feedback/details/279444/arrays-decay-into-pointers-in-conditional-operator - Ben Voigt

4
这种行为在[expr.typeid] / 2(N3936)中有所涉及:
当应用于多态类类型的glvalue表达式时,typeid引用表示glvalue引用的最派生对象(即动态类型)的类型的std :: type_info对象。如果通过将一元*运算符应用于指针来获得glvalue表达式,并且指针是null指针值,则typeid表达式会抛出一个与std :: bad_typeid异常类型匹配的异常。
表达式1?*p:*p始终是lvalue。这是因为*p是lvalue,并且[expr.cond] / 4表示如果三元运算符的第二个和第三个操作数具有相同的类型和值类别,则运算符的结果也具有该类型和值类别。
因此,1?*m_basePtr:*m_basePtr是具有类型Base的lvalue。由于Base具有虚拟析构函数,因此它是多态类类型。
因此,此代码确实是“当应用于多态类类型的glvalue表达式时typeid”的示例。
现在我们可以阅读上述引文的其余部分。 glvalue表达式不是“通过将一元*运算符应用于指针”获得的-它是通过三元运算符获得的。因此,标准不要求如果m_basePtr为空则抛出异常。
如果m_basePtr为空,则行为将由有关取消引用空指针的更一般规则(在C ++中有点模糊,但出于实际目的,我们将假定它在这里会导致未定义的行为)。
最后:为什么会有人写这个?我认为curiousguy的答案是迄今为止最可信的建议:通过这种构造,编译器不必插入空指针测试和生成异常的代码,因此它是微小优化。
假设程序员要么足够满意,认为永远不会使用null指针调用它,要么愿意依赖特定实现对null指针取消引用的处理。

0

我怀疑某个编译器在处理简单情况时出了问题。

typeid(*m_basePtr)

无论运行时类型如何,

返回typeid(Base)总是。 但将其转换为表达式/临时/右值会使编译器提供RTTI。

问题是哪个编译器,何时等。 我认为GCC早期对typeid存在问题,但这是一个模糊的记忆。


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