重新定义 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/ 到异常……仍然存在如何使用遗留代码的问题。
而且,总的来说 - 有几种错误报告方案。如果不同的库使用不同的方案,如何让它们一起生活。