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

时间:2013-02-12 20:26:44

标签: c++ c macros assert

重新定义断言宏是不是很邪恶?

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

我已经完成了

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

在上面的情况 - 代码已经使用assert,我想扩展断言失败的行为 - 当我想做像

这样的事情时

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

2)在断言

上自动调用调试器或堆栈跟踪 通过实现SIGABRT信号处理程序,可以在不重新定义断言的情况下完成

... this,2)。

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说:“一个不退出的断言的想法对我没有任何意义。”

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

E.g。而不是

#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)。 它可能是一个错误 - 在cygwin上,你可能需要调用cygwin_alternative_to_baz(1)。

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

是的:这不能保证有效。但是,就此而言,断言并不能保证工作,如果他们做的事情比调用中止处理程序更复杂。

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

甚至不保证能够正常工作(这次调用存在安全漏洞。)

E.g。如果malloc或sbrk失败了。

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

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

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

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

无论如何,使用assert()的代码是我的还是来自其他人:有时候你希望代码失败,通过调用SIGABRT或退出(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 (... ) {
      ...
   }

这比堆叠错误消息上下文更具风险,因为您可以继续操作。但是你总是可以选择cactch的例外类型。

元观察

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

如果这是正确的......如果错误报告中存在阶段性变化,从退出(1)/信号/到异常......仍然存在如何使用遗留代码的问题。

而且,整体而言 - 有几种错误报告方案。如果不同的图书馆使用不同的方案,那么如何让它们共同生活。

3 个答案:

答案 0 :(得分:10)

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

也就是说,如果翻译单元中的任何代码在您的定义之后重新定义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() /断言等迁移到异常。在许多情况下,没有办法有意义地继续处理,抛出异常只会产生怀疑,问题是否会被正确记录并导致预期的终止。

  

而且,整体而言 - 有几种错误报告方案。如果不同的图书馆使用不同的方案,那么如何让它们共同生活。

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

答案 1 :(得分:5)

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

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

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

答案 2 :(得分:2)

如果您包含使用assert的任何标头/库,那么您将遇到意外行为,否则编译器允许您这样做,以便您可以这样做。

我的建议基于个人意见,无论如何,您可以定义自己的断言,而无需重新定义现有断言。在重新定义现有版本而不是使用新名称定义新版本时,您永远不会获得额外的好处。