15

重新定义 assert 宏是不是很邪恶?

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

我已经做好了

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

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

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 baz\n";
    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)。

但是用户可能会理解什么是“条”。

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

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

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

例如,如果 malloc 或 sbrk 失败。

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

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

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

哦,见鬼,我不妨承认:有时我写了这段遗留代码。我故意使用 assert() 来表示错误。因为我不能依赖用户执行 try/catch/throw - 事实上,通常必须在 C/C++ 环境中使用相同的代码。我不想使用我自己的 ASSERT 宏——因为不管你信不信,ASSERT 经常发生冲突。我发现到处都是 FOOBAR_ASSERT() 和 A_SPECIAL_ASSERT() 丑陋的代码。不......简单地使用 assert() 本身很优雅,基本上可以工作。并且可以扩展....如果可以覆盖assert()。

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

我知道如何测试因 exit(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 一起认为,异常是报告想要安全的代码中的错误的最知名方法。(因为程序员不会忘记检查错误返回码。)

如果这是正确的……如果错误报告发生阶段性变化,从 exit(1)/signals/ 到异常……仍然存在如何使用遗留代码的问题。

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

4

3 回答 3

11

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

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

如果你的assert替代代码没有exit,你就会打开新的问题。在某些病态的边缘情况下,您关于投掷的想法可能会失败,例如:

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

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

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

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

返回错误值和使用异常之间的选择是您提到的那种参数/偏好的传统基础,因为它们是更合理的选择。

如果这是正确的……如果错误报告发生阶段性变化,从 exit(1)/signals/ 到异常……仍然存在如何使用遗留代码的问题。

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

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

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

于 2013-02-13T05:37:36.893 回答
8

我编写了一个在嵌入式系统上运行的应用程序。在早期,我在代码中随意地使用断言,表面上是为了记录代码中应该不可能的条件(但在一些地方作为惰性错误检查)。

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

无论您是否认为重新定义断言是“邪恶的”,这对我们都很有效。

于 2013-02-13T08:26:20.033 回答
2

如果您包含任何使用assert.

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

于 2013-02-12T20:32:26.217 回答