重新定义断言是邪恶的吗?

Is it evil to redefine assert?

本文关键字:邪恶 断言 新定义 定义      更新时间:2023-10-16

重新定义断言宏是邪恶的吗?

有些人建议使用你自己的宏ASSERT(cond(,而不是重新定义现有的标准assert(cond(宏。 但是,如果您有很多使用 assert(( 的遗留代码,您不想对其进行源代码更改,想要拦截、规范化断言报告,则这无济于事。

我做过

 #undef assert
 #define assert(cond)  ... my own assert code ...

在上述情况下 - 代码已经在使用 assert,我想扩展断言失败行为 - 当我想做类似的事情时

1(打印额外的错误信息以使断言更有用

2( 在断言上自动调用调试器或堆栈轨道

。this, 2(,可以通过实现 SIGABRT 信号处理程序来完成而无需重新定义断言。

3(将断言失败转换为抛出。

。this, 3(,不能由信号处理程序完成 - 因为你不能从信号处理程序抛出C++异常。 (至少不可靠。

我为什么要做断言抛出?堆叠错误处理。

我这样做通常不是因为我希望程序在断言后继续运行(尽管见下文(,而是因为我喜欢使用异常来提供更好的错误上下文。 我经常这样做:

int main() {
  try { some_code(); }
  catch(...) { 
     std::string err = "exception caught in command foo";
     std::cerr << err;
     exit(1);;
  }
}
void some_code() { 
  try { some_other_code(); }
  catch(...) { 
     std::string err = "exception caught when trying to set up directories";
     std::cerr << err;
     throw "unhandled exception, throwing to add more context";
  }
}
void some_other_code() { 
  try { some_other2_code(); }
  catch(...) { 
     std::string err = "exception caught when trying to open log file " + logfilename;
     std::cerr << err;
     throw "unhandled exception, throwing to add more context";
  }
}

等。

即异常处理程序添加更多的错误上下文,然后重新抛出。

有时我会打印异常处理程序,例如打印到 stderr。

有时我会将异常处理程序推送到一堆错误消息上。(显然,当问题内存不足时,这将不起作用。

** 这些断言异常仍然退出 ... **

@IanGoldby,有人评论这篇文章说:"断言不退出的想法对我来说没有任何意义。

以免我不清楚:我通常会有这样的例外退出。 但最终,也许不会立即。

例如,代替

#include <iostream>
#include <assert.h>
#define OS_CYGWIN 1
void baz(int n)
{
#if OS_CYGWIN
  assert( n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin." );
#else
  std::cout << "I know how to do baz(n) most places, and baz(n), n!=1 on Cygwin, but not baz(1) on Cygwin.n";
#endif
}
void bar(int n)
{
  baz(n);
}
void foo(int n)
{
  bar(n);
}
int main(int argc, char** argv)
{
  foo( argv[0] == std::string("1") );
}

仅生产

% ./assert-exceptions
assertion "n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin."" failed: file "assert-exceptions.cpp", line 9, function: void baz(int)
/bin/sh: line 1: 22180 Aborted                 (core dumped) ./assert-exceptions/
%

你可能会这样做

#include <iostream>
//#include <assert.h>
#define assert_error_report_helper(cond) "assertion failed: " #cond
#define assert(cond)  {if(!(cond)) { std::cerr << assert_error_report_helper(cond) "n"; throw assert_error_report_helper(cond); } }
     //^ TBD: yes, I know assert needs more stuff to match the definition: void, etc.
#define OS_CYGWIN 1
void baz(int n)
{
#if OS_CYGWIN
  assert( n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin." );
#else
  std::cout << "I know how to do baz(n) most places, and baz(n), n!=1 on Cygwin, but not baz(1) on Cygwin.n";
#endif
}
void bar(int n)
{
  try {
baz(n);
  }
  catch(...) {
std::cerr << "trying to accomplish bar by bazn";
    throw "bar";
  }
}
void foo(int n)
{
  bar(n);
}
int secondary_main(int argc, char** argv)
{
     foo( argv[0] == std::string("1") );
}
int main(int argc, char** argv)
{
  try {
return secondary_main(argc,argv);
  }
  catch(...) {
std::cerr << "main exiting because of unknown exception ...n";
  }
}

并获得稍微更有意义的错误消息

assertion failed: n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin."
trying to accomplish bar by baz
main exiting because of unknown exception ...

我不应该解释为什么这些上下文相关的错误消息更有意义。例如,用户可能根本不知道为什么调用 baz(1(。这很可能是一个 pogram 错误 - 在 cygwin 上,您可能需要调用 cygwin_alternative_to_baz(1(。

但是用户可能理解"bar"是什么。

是的:这不能保证有效。 但是,就此而言,断言不能保证有效,如果断言执行比调用中止处理程序更复杂的操作。

write(2,"error baz(1) has occurred",64);

即使这样也不能保证有效(此调用中存在安全错误。

例如,如果 malloc 或 sbrk 出现故障。

我为什么要做断言抛出?测试

我偶尔重新定义断言的另一个重要原因是为遗留代码编写单元测试,这些代码使用 assert 来指示错误,我不允许重写。

如果此代码是库代码,则通过 try/catch 包装调用很方便。 查看是否检测到错误,然后继续。

哦,哎呀,我不妨承认这一点:有时我写了这个遗留代码。 我故意使用 assert(( 来发出错误信号。 因为我不能依赖用户执行try/catch/throw-实际上,通常必须在C/C++环境中使用相同的代码。 我不想使用我自己的 ASSERT 宏 - 因为,信不信由你,ASSERT 经常冲突。 我发现充斥着 FOOBAR_ASSERT(( 和 A_SPECIAL_ASSERT(( 的代码很丑陋。 不。。。 简单地使用 assert(( 本身是优雅的,基本上有效。 并且可以扩展。如果可以覆盖断言((。

无论如何,无论使用 assert(( 的代码是我的还是来自其他人的:有时您希望代码失败,通过调用 SIGABRT 或 exit(1( - 有时您希望它抛出。

我知道如何测试因退出(a(或SIGABRT而失败的代码 - 类似

for all  tests do
   fork
      ... run test in child
   wait
   check exit status

但是这段代码很慢。并不总是便携式的。并且经常运行速度慢几千倍

for all  tests do
   try {
      ... run test in child
   } catch (... ) {
      ...
   }

这比仅仅堆叠错误消息上下文风险更大,因为您可能会继续操作。 但是,您始终可以选择例外类型来控制。

元观察

我和Andrei Alexandresciu一样认为异常是报告想要安全的代码中的错误的最知名方法。 (因为程序员不能忘记检查错误返回代码。

如果这是正确的...如果错误报告有相变,从退出(1(/信号/到异常...人们仍然有如何忍受遗留代码的问题。

而且,总的来说 - 有几种错误报告方案。 如果不同的库使用不同的方案,如何使它们共存。

重新定义标准宏是一个丑陋的想法,你可以确定行为在技术上是未定义的,但最终宏只是源代码替换,很难看出它如何导致问题,只要断言导致程序退出。

也就是说,如果你的定义本身重新定义翻译单元中的任何代码,你的预期替换可能不会被可靠地使用assert,这表明需要特定的包含顺序等 - 该死的脆弱。

如果你的assert替换了不exit的代码,你就会遇到新的问题。 在某些病态的边缘情况下,你关于投掷的想法可能会失败,例如:

int f(int n)
{
    try
    {
        assert(n != 0);
        call_some_library_that_might_throw(n);
    }
    catch (...)
    {
        // ignore errors...
    }
    return 12 / n;
}

上面,n的值 0 开始使应用程序崩溃,而不是通过理智的错误消息停止它:抛出的消息中的任何解释都不会被看到。

我和Andrei Alexandresciu一样认为异常是报告想要安全的代码中的错误的最知名方法。(因为程序员不能忘记检查错误返回代码。

我不记得安德烈说过——你有报价吗? 他当然非常仔细地考虑过如何创建鼓励可靠异常处理的对象,但我从未听过/见过他建议在某些情况下停止程序断言是不合适的。 断言是强制执行不变量的正常方式 - 肯定有一条线可以确定哪些潜在的断言可以继续,哪些不能,但在这条线的一侧,断言仍然有用。

返回错误值和使用异常之间的选择是您提到的参数/首选项类型的传统基础,因为它们是更合法的替代方案。

如果这是正确的...如果错误报告有相变,从退出(1(/信号/到异常...人们仍然有如何忍受遗留代码的问题。

如上所述,您不应该尝试将所有现有的exit()/断言等迁移到异常。 在许多情况下,无法有意义地继续处理,抛出异常只会让人怀疑问题是否会被正确记录并导致预期的终止。

而且,总的来说 - 有几种错误报告方案。如果不同的库使用不同的方案,如何使它们共存。

如果这成为一个真正的问题,您通常会选择一种方法,并将不合格的库包装为提供您喜欢的错误处理的层。

我编写了一个在嵌入式系统上运行的应用程序。在早期,我自由地在代码中散布断言,表面上是为了记录代码中应该不可能的条件(但在少数地方是懒惰的错误检查(。

事实证明,断言偶尔会被击中,但没有人看到输出到控制台的消息包含文件和行号,因为控制台串行端口通常没有连接到任何东西。我后来重新定义了断言宏,以便它不会将消息输出到控制台,而是通过网络将消息发送到错误记录器。

无论你是否认为重新定义断言是"邪恶的",这对我们来说都很有效。

如果包含任何使用 assert 的标头/库,则会遇到意外行为,否则编译器允许您执行此操作,以便您可以执行此操作。

我的建议是基于个人意见的,在任何情况下,你都可以定义自己的断言,而不需要重新定义现有的断言。与使用新名称定义新名称相比,重新定义现有名称永远不会获得额外的好处。