【问题标题】:How to catch undefined behaviour in function argument initialization如何在函数参数初始化中捕获未定义的行为
【发布时间】:2019-05-15 15:02:09
【问题描述】:

以下代码在 clang++ 中工作,但在 g++ 中崩溃了

#include<vector>
#include<iostream>

template<class Iterator>
double abs_sum(double current_sum, Iterator it, Iterator it_end){
    if (it == it_end)
        return current_sum;
    return abs_sum(current_sum+std::abs(*it),++it,it_end);
}


int main(int argc, char** argv){
    std::vector<double> values {1.0, 2.0,-5};

    std::cout << abs_sum(0.0,values.begin(),values.end()) << std::endl;;
}

罪魁祸首原来是这行:

return abs_sum(current_sum+std::abs(*it),++it,it_end);

在 clang 中,*it++it 之前评估,在 g++ 中则相反,导致迭代器在被取消引用之前增加。事实证明,评估函数参数的顺序是实现定义的。

我的问题是:我如何捕捉这种类型的错误?理想情况下,当我不小心依赖于实现的具体细节时,我希望有一个错误或至少一个警告。

clang 和 gcc 都不会产生任何警告,即使使用 -Wall。

【问题讨论】:

  • 在调用函数之前按正确的顺序执行操作。
  • 请定义“壮观的崩溃” :D 它包括声音和灯光吗?
  • 添加另一个单元测试。
  • @Bernhard 单元测试在这里不一定对你有帮助。如果你有 UB,那么任何事情都可能发生,你不能依靠测试来捕捉它。这是最难发现的错误类别,最终它实际上只是代码审查和纯属偶然。我确实同意这样的测试应该存在,因为如果你经常运行你的单元测试,那么你会增加这个错误弹出并因此被发现的机会;但是,该错误很可能仅在更复杂的程序中出现症状。
  • 这些很难捕捉,我们可以找到各种难以捕捉的有趣案例,例如this onethis one。这是changing the evaluation order rules in C++17 的动机,但我们没有确定这个具体的。

标签: c++ undefined-behavior unspecified-behavior


【解决方案1】:

我的问题是:我如何捕捉这种类型的错误?

你没有。未定义的行为是未定义的。你抓不住它...

...但有些工具可以帮助您:

  • 您的编译器:启用所有警告(g++/clang++ -Wall -Wextra -pedantic 是一个好的开始);
  • cppcheck;
  • clang 分析器;

但他们不提供任何保证。这就是为什么 C++困难。您(您,编码员)最了解并且不要编写 UB。祝你好运。

【讨论】:

  • 当然存在一些有助于防御 UB 的警告。例如在初始化之前使用的变量
  • @LightnessRacesinOrbit 是的,添加了。
【解决方案2】:

不幸的是,即使使用-Wextra(请记住,-Wall 更像-Wsome 因此不足),也没有任何警告,这有点令人失望。

在一个更简单的情况下,使用原语,种族*对编译器来说更明显:

void foo(int, int) {}

int main()
{
    int x = 42;
    foo(++x, x);
}

…你警告:

main.cpp: In function 'int main()':
main.cpp:6:9: warning: operation on 'x' may be undefined [-Wsequence-point]
     foo(++x, x);
         ^~~
main.cpp:6:9: warning: operation on 'x' may be undefined [-Wsequence-point]

(* 不是真正的种族,但你知道我的意思)

但是编译器很难“知道”您对迭代器的操作分别是读取和写入。

恐怕最终你将不得不依靠测试、智慧和进取心。 :)

【讨论】:

  • 我曾希望能够检查同一对象上对非常量函数的多次调用。一些误报将是可以接受的价格。我也很惊讶当迭代器超出范围时地址清理程序没有捕获。
  • @LKlevin 我认为编译器开发人员已经决定,所说的误报将是不可接受的价格。但探索更多可能会很有趣。
【解决方案3】:

你最初拥有的不是未定义的行为,而是未指定的行为。编译器不需要为未指定的行为发出任何诊断。

Order of evaluation 几乎所有 C++ 运算符的操作数(包括函数调用表达式中函数参数的求值顺序和任何表达式中子表达式的求值顺序)未指定。编译器可以按任何顺序计算操作数,并且在再次计算同一表达式时可能会选择其他顺序。

但在这种情况下,这种未指定行为的结果会导致结束迭代器的取消引用,这反过来又会导致未定义的行为。


GCC 和 Clang 没有任何通用编译器选项来为未指定的行为发出诊断。

在 GCC 中有一个选项 fstrong-eval-order,它执行以下操作:

按照 C++17 采用的从左到右的顺序计算成员访问、数组下标和移位表达式,并按照从右到左的顺序计算赋值。默认情况下使用-std=c++17 启用。 -fstrong-eval-order=some 仅启用成员访问和移位表达式的排序,并且是没有-std=c++17 的默认值。

还有一个选项 -Wreorder(仅限 C++ 和 Objective-C++):

当代码中给出的成员初始化器的顺序与它们必须执行的顺序不匹配时发出警告

但我认为这些选项对您的特定情况没有帮助。

所以在这种特殊情况下,您可以按预期的顺序执行操作。

【讨论】:

  • 未指定可能会导致取消引用结束迭代器所以 UB。
猜你喜欢
  • 2015-08-21
  • 2012-03-24
  • 1970-01-01
  • 1970-01-01
  • 2020-08-07
  • 1970-01-01
  • 2018-07-16
  • 2014-07-21
  • 2016-06-03
相关资源
最近更新 更多