PVS-Studio的兔子洞深度或C ++采访

PVS-Studio上的C ++访谈

作者:Andrey Karpov, Khandeliants菲利普·汉德利安特。

当我们在访谈中使用的问题竟比作者预期的复杂时,我想分享一个有趣的情况。使用C ++语言和编译器,您应该始终保持警惕。不要觉得无聊。

与任何其他编程公司一样,对于C ++,C#和Java开发人员的职位空缺,我们也有一些问题要采访。我们有双重底数或三重底数的许多问题。对于C#和Java的问题,我们不确定,因为他们还有其他作者。但是,安德烈·卡波夫(Andrei Karpov)为C ++访谈编写的许多问题被立即构想为探究语言功能知识的深度。

这些问题可以给一个简单的正确答案。您可以做得越来越深。以此为基础,在面试中,我们确定一个人对这种语言的细微程度的熟悉程度。这对于我们很重要,因为我们正在开发代码分析器,并且应该很好地理解语言的细微差别和“笑话”。

今天,我们将讲一个简短的故事,说明面试中提出的第一个问题竟然比我们计划的要深。因此,我们显示以下代码:

void F1()
{
  int i = 1;
  printf("%d, %d\n", i++, i++);
}

并问:“将打印什么?”

好问题。马上可以讲很多关于知识的知识。一个人根本无法回答的情况将不予考虑。通过在HeadHunter网站(hh.ru)上进行初步测试可以消除这些问题。虽然,不,这是胡扯。在我们的记忆中,有几个独特的性格在精神上回答了这些问题:

此代码将在开头打印一个百分比,然后是d,然后是另一个百分比d,然后是魔杖n,然后是两个单位。

显然,在这种情况下,面试很快结束。

所以,现在回到正常的面试:)。他们通常会这样回答:

打印1和2,

这是实习生级别的答案。是的,当然可以打印出这些值,但我们正在等待大约以下答案:

这并不是说此代码将确切打印出什么。这是未指定(或未定义)的行为。没有定义参数的计算顺序。在执行被调用函数的主体之前,必须对所有参数进行求值,但是编译过程中将自行决定执行的顺序。因此,该代码可以很好地打印“ 1、2”,反之亦然,同时打印“ 2、1”。通常,编写这样的代码是非常不希望的,如果要由至少两个编译器来构建,则有可能“步履维艰”。许多编译器会在此处发出警告。

确实,如果使用Clang,则可以得到“ 1、2”。

如果使用GCC,则可以得到“ 2,1”。

曾几何时,我们尝试了MSVC编译器,它也产生了“ 2,1”。没有麻烦的迹象。

最近,出于一般第三方的目的,再次有必要使用现代Visual C ++编译此代码并运行它。在启用/ O2优化的发布配置下组装。而且,正如他们所说,他们发现冒险是他们自己的头:)。您认为发生了什么事?哈!内容如下:“ 1,1”。

所以想你想要什么。事实证明,这个问题更加深刻和令人困惑。我们自己没想到会发生这种情况。

由于C ++标准不以任何方式规范参数的计算,因此编译器以非常特殊的方式解释这种类型的未指定行为。让我们看一下由MSVC 19.25编译器(Microsoft Visual Studio Community 2019版本16.5.1)生成的汇编代码,该语言标准的标记版本为'/ std:c ++ 14':


正式地,优化器将上面的代码转换为以下代码:

void F1()
{
  int i = 1;
  int tmp = i;
  i += 2;
  printf("%d, %d\n", tmp, tmp);
}

从编译器的角度来看,这种优化不会改变程序的行为。鉴于此,您将有充分的理由开始理解,C ++ 11标准除了智能指针外,还添加了“魔术”函数make_shared(C ++ 14还添加了make_unique)。这样一个无害的例子,也可以“破柴”:

void foo(std::unique_ptr<int>, std::unique_ptr<double>);

int main()
{
  foo(std::unique_ptr<int> { new int { 0 } },
      std::unique_ptr<double> { new double { 0.0 } });
}

狡猾的编译器可以将其转换为以下计算顺序(例如,相同的MSVC):

new int { .... };
new double { .... };
std::unique_ptr<int>::unique_ptr
std::unique_ptr<double>::unique_ptr

如果对new运算符的第二次调用引发异常,则将发生内存泄漏。

但是回到原来的话题。尽管从编译器的角度来看一切都很好,但我们仍然可以确保输出“ 1,1”在考虑开发人员预期的行为方面是不正确的。然后,我们尝试使用带有标准版本标记'/ std:c ++ 17'的MSVC编译器来编译源代码。一切开始按预期工作,并打印“ 2,1”。看一下汇编代码:


一切都是公平的,编译器将值2和1作为参数传递,但是为什么一切都发生了如此巨大的变化?事实证明,以下内容已添加到C ++ 17标准中:

postfix-expression在expression-list中的每个表达式和任何默认参数之前被排序。相对于任何其他参数的初始化,不确定地排序一个参数的初始化,包括每个相关的值计算和副作用。

编译器仍然有权以任意顺序计算参数,但是现在,从C ++ 17标准开始,编译器有权从下一个参数的所有计算和副作用完成之时开始计算下一个参数及其副作用。

顺便说一句,如果您使用带有'/ std:c ++ 17'标志的智能指针编译相同的示例,那么在那里一切都变得很好 -使用std :: make_unique现在是可选的。

这是事实证明深度的另一种衡量标准。有理论,但是有实践的形式是特定的编译器或对标准的不同解释:)。 C ++的世界总是比看起来更加复杂和出乎意料。

如果有人可以更准确地解释正在发生的事情,请在评论中告诉我们。我们最终必须了解这个问题,以便至少让我们自己在面试中知道答案! :)

这是一个内容丰富的故事。我们希望这很有趣,并希望您对此话题发表意见。并且我们建议尽可能使用最现代的语言标准,以免让当前的优化编译器感到惊讶。更好的是,根本不用编写此代码:)。

PS:我们可以说我们“解决了问题”,现在必须将其从调查表中删除。我们看不到这一点。如果一个人在采访前不太懒于研究我们的出版物,阅读并使用了该材料,那么他做得很好,当之无愧地会收到一个加号:)。


如果您想与讲英语的读者分享这篇文章,请使用以下链接:Andrey Karpov,Phillip Khandeliants。PVS-Studio上的“兔子洞”的深度或C ++求职面试的深度

All Articles