重新定义断言是邪恶的吗?
Is it evil to redefine assert?
重新定义断言宏是邪恶的吗?
有些人建议使用你自己的宏ASSERT(cond(,而不是重新定义现有的标准assert(cond(宏。 但是,如果您有很多使用 assert(( 的遗留代码,您不想对其进行源代码更改,想要拦截、规范化断言报告,则这无济于事。
我做过
#undef assert
#define assert(cond) ... my own assert code ...
在上述情况下 - 代码已经在使用 assert,我想扩展断言失败行为 - 当我想做类似的事情时
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 bazn";
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(。
但是用户可能理解"bar"是什么。
是的:这不能保证有效。 但是,就此而言,断言不能保证有效,如果断言执行比调用中止处理程序更复杂的操作。
write(2,"error baz(1) has occurred",64);
即使这样也不能保证有效(此调用中存在安全错误。
例如,如果 malloc 或 sbrk 出现故障。
我为什么要做断言抛出?测试
我偶尔重新定义断言的另一个重要原因是为遗留代码编写单元测试,这些代码使用 assert 来指示错误,我不允许重写。
如果此代码是库代码,则通过 try/catch 包装调用很方便。 查看是否检测到错误,然后继续。
哦,哎呀,我不妨承认这一点:有时我写了这个遗留代码。 我故意使用 assert(( 来发出错误信号。 因为我不能依赖用户执行try/catch/throw-实际上,通常必须在C/C++环境中使用相同的代码。 我不想使用我自己的 ASSERT 宏 - 因为,信不信由你,ASSERT 经常冲突。 我发现充斥着 FOOBAR_ASSERT(( 和 A_SPECIAL_ASSERT(( 的代码很丑陋。 不。。。 简单地使用 assert(( 本身是优雅的,基本上有效。 并且可以扩展。如果可以覆盖断言((。
无论如何,无论使用 assert(( 的代码是我的还是来自其他人的:有时您希望代码失败,通过调用 SIGABRT 或 exit(1( - 有时您希望它抛出。
我知道如何测试因退出(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一样认为异常是报告想要安全的代码中的错误的最知名方法。 (因为程序员不能忘记检查错误返回代码。
如果这是正确的...如果错误报告有相变,从退出(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一样认为异常是报告想要安全的代码中的错误的最知名方法。(因为程序员不能忘记检查错误返回代码。
我不记得安德烈说过——你有报价吗? 他当然非常仔细地考虑过如何创建鼓励可靠异常处理的对象,但我从未听过/见过他建议在某些情况下停止程序断言是不合适的。 断言是强制执行不变量的正常方式 - 肯定有一条线可以确定哪些潜在的断言可以继续,哪些不能,但在这条线的一侧,断言仍然有用。
返回错误值和使用异常之间的选择是您提到的参数/首选项类型的传统基础,因为它们是更合法的替代方案。
如果这是正确的...如果错误报告有相变,从退出(1(/信号/到异常...人们仍然有如何忍受遗留代码的问题。
如上所述,您不应该尝试将所有现有的exit()
/断言等迁移到异常。 在许多情况下,无法有意义地继续处理,抛出异常只会让人怀疑问题是否会被正确记录并导致预期的终止。
而且,总的来说 - 有几种错误报告方案。如果不同的库使用不同的方案,如何使它们共存。
如果这成为一个真正的问题,您通常会选择一种方法,并将不合格的库包装为提供您喜欢的错误处理的层。
我编写了一个在嵌入式系统上运行的应用程序。在早期,我自由地在代码中散布断言,表面上是为了记录代码中应该不可能的条件(但在少数地方是懒惰的错误检查(。
事实证明,断言偶尔会被击中,但没有人看到输出到控制台的消息包含文件和行号,因为控制台串行端口通常没有连接到任何东西。我后来重新定义了断言宏,以便它不会将消息输出到控制台,而是通过网络将消息发送到错误记录器。
无论你是否认为重新定义断言是"邪恶的",这对我们来说都很有效。
如果包含任何使用 assert
的标头/库,则会遇到意外行为,否则编译器允许您执行此操作,以便您可以执行此操作。
我的建议是基于个人意见的,在任何情况下,你都可以定义自己的断言,而不需要重新定义现有的断言。与使用新名称定义新名称相比,重新定义现有名称永远不会获得额外的好处。
- 尝试使用 std::vector<std::thread时出现静态断言失败错误>
- uint_not_usable_without_attribute在业力规则中使用数字生成器时静态断言失败
- C++ 使用增强正则表达式库时断言崩溃
- 从 exe 文件 (Visual Studio ) 启动时调试断言失败
- 如何将向量断言到特征矩阵
- OpenCV - Python 断言错误:SAD 算法 - 立体相机视差图计算
- 使用 Google Test 对自定义断言函数进行单元测试
- 断言"id < 0"在Qt ActiveX中失败
- 初始值设定项列表构造和静态断言
- 在 CppUnit 中测试中止断言失败
- 使用扫描的调试断言失败
- MS 本机单元测试 - 断言::线程失败不起作用
- 如何断言 CRTP 的函数为最终函数?
- 迭代器跳闸视觉C++ 2017 断言
- 如何在 google test in windows 中管理断言
- 为什么我的Qt程序在断言失败后继续运行?
- 在C++中禁用断言宏
- 宏"断言"会在 C++20 中删除吗?
- 在 constexpr 函数中断言
- 重新定义断言是邪恶的吗?