【问题标题】:Is it evil to redefine assert?重新定义断言是邪恶的吗?
【发布时间】:2015-09-14 11:32:43
【问题描述】:

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

有些人建议使用您自己的宏 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";
  }
}

等等

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

有时我会打印异常处理程序,例如到标准错误。

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

** 这些断言异常仍然存在... **

对此帖子发表评论的人@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/ 到异常……仍然存在如何使用遗留代码的问题。

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

【问题讨论】:

  • 这可能更适合程序员,因为您正在寻找对编程文化的共识,而不是特定编程问题的解决方案。
  • 您应该定义自己的 ASSERT。从技术上重新定义标准可能会导致未定义的行为。如果您有很多遗留代码需要更改,那么您需要一个好的代码编辑器或带有重构工具的 IDE。
  • 虽然我同意,自己的定义可以避免一些问题,但我不认为重新定义 assert() 是邪恶的,因为每个编译单元都可以这样做。我在某些平台上自己做,语言规范也不气馁:参见 7.2.1.1 C99 Rationale 5.10 中的断言宏:open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf
  • 大多数 人建议编写自己的断言 :-)
  • @AdrianMcCarthy:当您不允许编辑代码时,重构工具无济于事。或者至少不应该。例如。当代码来自其他地方、外部提供商并且必须跟踪时。 // Michael Feathers 在他的“Working with Legacy Code”一书中,谈到了当你无法编辑代码时的几种重构方法。宏重定义是标准方式之一。

标签: c++ c macros assert


【解决方案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 的观点,认为异常是报告需要安全的代码中的错误的最知名方法。 (因为程序员不会忘记检查错误返回码。)

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

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

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

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

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

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

【讨论】:

  • 断言不退出的想法对我来说没有任何意义。断言只能用于记录开发人员认为在代码中的特定点必须为真的条件,除非代码逻辑中存在缺陷。它们用于捕获错误,而不是无效数据或运行时错误。如果您有错误(除非在非常特殊的情况下),您将不再了解进程或其数据的完整性,因此您所能做的就是停止。 (如果幸运的话,您可以在停止之前将输出驱动到安全状态。)
  • @IanGoldby:是的 - 我同意,但这个问题专门假设了这种用法。我不想被拖入对程序员倾向于选择其中一种情况的解释中——只是说明所提出的技术甚至不够稳健。
  • @TonyD:Andrei 在他关于 D 编程语言的演讲中支持错误处理的异常,我参加并做了笔记:blog.andy.glew.ca/2009/07/… // 不是针对 Andrei,但 Andrei 非常重视与D有关,D说:dlang.org/overview.html异常处理。越来越多的异常处理经验表明,与使用错误代码和 errno 全局变量的 C 传统方法相比,它是一种更好的错误处理方法。 // D异常比C++好,但我觉得C++是可以用的。
  • @IanGoldby:我对“Out-of-the-Blue”异常的首选使用模型是让它们退出 - 但不是立即退出。相反,它们打印一条错误消息,然后抛出;一些外部级别打印一条错误消息,然后抛出;通常在最外层,main,我打印最后的错误信息,然后 exit(1)。基本上,我使用异常来为错误提供更多上下文。 // 嗯...我会在问题中解释。
  • @KrazyGlew 即使在你上面所说的“处理错误的优于使用错误代码和 errno 全局变量的 C 传统方法”中,你也没有比较异常和断言,正如我尝试过的那样在我的回答中解释运行时处理的问题和代码质量验证机制之间的概念线的不同方面。
【解决方案2】:

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

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

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

【讨论】:

  • @Ian Goldby - 感谢您提醒我:我做了几乎完全相同的事情。在一个案例中,我为一台驱动信息亭中的多个显示器的 PC 编写了一个数据库。编译器供应商提供的旧版 assert() 宏写入 PC 主监视器。必须更改它们以记录到可以通过电话线检索的日志。
【解决方案3】:

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

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

【讨论】:

  • 编译器吞下某些代码并不意味着它是合法的、标准的
  • @PlasmaHH 我从来没有说过。有时,此类重新定义有其客观的用例,进一步回答会引发讨论。
猜你喜欢
  • 2010-12-23
  • 2011-01-02
  • 1970-01-01
  • 2011-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-18
相关资源
最近更新 更多