重新定义断言宏是不是很邪恶?
有些人建议使用您自己的宏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)/信号/到异常......仍然存在如何使用遗留代码的问题。
而且,整体而言 - 有几种错误报告方案。如果不同的图书馆使用不同的方案,那么如何让它们共同生活。
答案 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
的任何标头/库,那么您将遇到意外行为,否则编译器允许您这样做,以便您可以这样做。
我的建议基于个人意见,无论如何,您可以定义自己的断言,而无需重新定义现有断言。在重新定义现有版本而不是使用新名称定义新版本时,您永远不会获得额外的好处。