在C/C++中,assert(false)的更好替代方案是什么?

17

目前,我在编写

assert(false);

在我的代码从未到达的地方。其中一个例子是,用一种非常类似C语言的风格编写的:

int findzero( int length, int * array ) {
  for( int i = 0; i < length; i++ )
    if( array[i] == 0 )
      return i;
  assert(false);
}

当 assert(false) 被执行时,我的编译器会认为程序结束。但是出于性能原因,每次使用 -DNDEBUG 进行编译时,最后的断言会消失,并且编译器会警告执行结束函数时缺少返回语句。

如果我们需要结束一个程序,但实际上无法到达代码的某个部分,有哪些更好的替代方案呢?该解决方案应

  • 被编译器识别并且不产生警告(如上述警告或其他警告)
  • 允许自定义错误消息

我特别关注的是解决方案,无论是现代C ++还是90年代的 C。


3
抛出异常? - user2100815
3
abort()?虽然我不确定自己是否正确理解了问题。 - Ayxan Haqverdili
1
如果数组中真的找不到 0,你的代码应该如何表现?你假设 0 在数组中存在。这有什么保证吗? - abelenky
2
C/C++中assert(false)的更好替代方案是什么?在生产代码中加入实际的错误处理怎么样? - Lundin
2
我绝不会让“无法到达”的代码通过代码审查。这是一个设计缺陷。调用者知道array存储了什么,他有责任断言不可能的情况。让findzero返回一个可选项并在调用站点上进行断言。 - local-ninja
显示剩余2条评论
9个回答

15

用"unreachable"内建函数来替代assert(false)是很恰当的。

它们在语义上等同于你使用assert(false),实际上,VS的“unreachable”内建函数拼写非常相似。

GCC/Clang/Intel:

__builtin_unreachable()

MSVS:

 __assume(false)

这些代码具有影响,无论是否定义了NDEBUG(与assert不同)或优化级别。

你的编译器,特别是使用上述内置函数,但也可能使用assert(false),会理解您的承诺,即该函数的某个部分永远不会被执行。它可以利用这一点来对某些代码路径执行一些优化,并且它将消除关于缺少返回的警告,因为您已经“承诺”这是故意的。

这种情况的折衷是该语句本身具有未定义行为(就像已经向前流动并超出函数末尾一样)。在某些情况下,您可能希望考虑抛出异常(或返回某个“错误代码”值),或者在需要终止程序时调用std::abort()(在C++中)。


有一个提案(P0627R0),将此作为标准属性添加到C++中。


根据GCC文档中的Builtins

如果程序执行到了__builtin_unreachable,则程序行为未定义。在编译器无法推断代码不可达的情况下,它很有用。[..]


1
为什么不直接调用 std::abort() 呢? - Ayxan Haqverdili
1
@Ayxan 因为 std::abort() 承诺了特定的行为。而 __assume(false) 则没有这样的承诺。 - Caleth
1
@Ayxan 因为 std::abort 是一种不同的东西,具有不同的含义和不同的效果。内置函数是用于当你知道某些代码不会被执行时(除非所有的赌注都已经失效了);而 abort 则是用于当你想要捕获到某些代码被执行时。 - Lightness Races in Orbit
@Ayxan 有趣的措辞。不过你好像在暗示这些内置函数是通过“间接”的方式调用 std::abort() 的? - Lightness Races in Orbit
这似乎是最接近我所寻找的东西了。唯一的缺点是它在技术上依赖于供应商,尽管在不同的编译器中几乎是一个标准。我相信像这样的东西应该被标准化。 - shuhalo
显示剩余3条评论

6
作为一个完全可移植的解决方案,考虑以下内容:
[[ noreturn ]] void unreachable(std::string_view msg = "<No Message>") {
    std::cerr << "Unreachable code reached. Message: " << msg << std::endl;
    std::abort();
}

消息部分当然是可选的。


1
在调用abort()之前不刷新流(以便消息被打印)是否安全? - nada
4
@nada std::cerr 应该在每次操作后自动刷新。 - HolyBlackCat
std::endl 也会刷新。 - user79878
1
@user79878 最初我使用的是 '\n',而不是 endl。随后我根据评论做出了更改。 - Ayxan Haqverdili

5

我喜欢使用

assert(!"This should never happen.");

...这也可以与条件一起使用,如下所示:

assert(!vector.empty() || !"Cannot take element from empty container." );

这样做的好处在于,如果一个断言不成立,该字符串会在错误消息中显示出来。


可以(有点安全地)将第二行包装到一个接受两个参数的宏中吗? - shuhalo
1
是的,但这并不能解决你的问题,因为 assert 本身就是一个宏,在 NDEBUG 构建中变为空。 - bug
1
@bug 是的,你说得对 -- 我认为我的回答应该是一条评论。我想提出一个稍微改进 assert(false); 的建议,以满足“允许自定义错误消息”的要求。 - Frerich Raabe

4

2
目前,std :: unreachable()只保证未定义行为。Ayxan Haqverdili的自定义unreachable()函数将更适合OP的目的。https://en.cppreference.com/w/cpp/utility/unreachable - Hari
是的,使用std::unreachable来确保程序正确性似乎是一个非常糟糕的想法。由于您正在调用未定义行为,这并不意味着:“如果程序正确,这永远不会发生,所以当它发生时我希望得到警告并且程序崩溃。”相反,您正在表明:“我确定这永远不会发生,编译器可以自由地进行优化!”这是我曾经见过的一个无聊的错误。 - rhaps0dy

2

我使用一个自定义的assert,当NDEBUG开启时会转换为__builtin_unreachable()*(char*)0=0(我还使用枚举变量而不是宏,以便可以轻松地在每个范围内设置NDEBUG)。

伪代码如下:

#define my_assert(X) do{ \ 
       if(!(X)){ \
           if (my_ndebug) MY_UNREACHABLE();  \
           else my_assert_fail(__FILE__,__LINE__,#X); \
       } \
     }while(0)

__builtin_unreachable() 可以消除警告并同时帮助优化代码,但在调试模式下最好使用 assert 或者 abort();,这样可以得到可靠的 panic。(__builtin_unreachable() 在被执行时会导致未定义的行为)。


我认为他们应该在C(或C ++)标准中包括一个类似于assert的标准化的“unreachable”宏。 - shuhalo
1
根据我的回答,有一个提议正在考虑这样做。 - Lightness Races in Orbit
1
@shuhalo 说实话,我讨厌委员会不必要地膨胀我的最爱编程语言。编译器可以将 *(char*)0=0; 识别为等同于 __builtin_unreachable();,因为它在语义上是等效的,这样就不需要任何提案或扩展了。 - Petr Skocik

1
我建议使用C++核心准则ExpectsEnsures。它们可以配置为在违规时中止(默认)、抛出异常或不做任何操作。
为了抑制编译器警告,您也可以使用GSL_ASSUME来处理无法到达的分支。
#include <gsl/gsl>

int findzero( int length, int * array ) {
  Expects(length >= 0);
  Expects(array != nullptr);

  for( int i = 0; i < length; i++ )
    if( array[i] == 0 )
      return i;

  Expects(false);
  // or
  // GSL_ASSUME(false);
}

0

assert 的使用场景是在程序执行时实际上不应该发生的情况下。它在调试中非常有用,可以指出“嘿,原来你认为不可能的情况实际上是可能的”。在给定的示例中,你应该表达函数的失败,可能通过返回 -1 来表示该索引无效。在某些情况下,设置 errno 可能也很有用,以澄清错误的确切性质。有了这些信息,调用函数就可以决定如何处理此类错误。

根据错误对应用程序的重要性程度,你可以尝试恢复,或者只记录错误并调用 exit 使其停止运行。


0

我相信你们之所以出现错误,是因为断言通常用于调试自己的代码。当这些函数在发布时运行时,应该改用异常,并使用 std::abort() 退出指示异常程序终止。

如果你仍然想使用断言,PSkocik提供了一个关于定义自定义断言的答案,还有一个链接here,其中有人建议使用自定义断言以及如何在cmake中启用它们。


-3

编程风格指南中有时会发现这样一条规则:

"不要从函数中间返回"
所有函数应该在函数末尾只有一个返回。

按照这个规则,你的代码看起来会像这样:

int findzero( int length, int * array ) {
  int i;
  for( i = 0; i < length; i++ )
  {
    if( array[i] == 0 )
      break;             // Break the loop now that i is the correct value
  }

  assert(i < length);    // Assert that a valid index was found.
  return i;              // Return the value found, or "length" if not found!
}

16
虽然这是个糟糕的规定。 - Lightness Races in Orbit
7
确实是一条非常糟糕的规则。按照这个规则做只会让你感到痛苦。SBRO / RAII 的整个思想就是允许早期返回,这极大地简化了程序代码,并使得对其进行更好的推理成为可能。 - SergeyA
3
既然这个问题被标记为跨标签[tag:c],那么我会允许你的论点 ;) - Lightness Races in Orbit
5
Goto有其合理的应用。让我们不要再重新开始这个话题了 :P - Lightness Races in Orbit
1
@SergeyA 包装函数 -> 调用初始化函数 -> 在错误时返回错误代码 -> 在包装函数中进行清理。比1980年代的BASIC语言中的“on error goto”更易读和易于维护。巧合的是,这也证明了为什么多个返回可以成为良好的编程实践。 - Lundin
显示剩余9条评论

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