【发布时间】: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”一书中,谈到了当你无法编辑代码时的几种重构方法。宏重定义是标准方式之一。