【问题标题】:How to override assert macro in C?如何覆盖 C 中的断言宏?
【发布时间】:2016-06-05 08:28:59
【问题描述】:

我想创建我自己的assert 版本,它会打印一些日志,以防在NDEBUG 模式下调用断言。

我尝试使用LD_PRELOAD 技巧并重新定义断言宏,但它似乎完全忽略了宏定义并且覆盖__assert_fail 是无关紧要的,因为在NDEBUG 的情况下不会调用它。

如何覆盖 libc assert 宏?

我不想创建不同的函数,因为 assert 已经在项目中大量使用。

【问题讨论】:

  • Assert 是一个宏,所以 LD_PRELOAD 技巧不适用。您将需要取消定义标准宏(或不包括定义它的头文件),然后定义您自己的断言宏 before 包括使用它的任何其他头文件。如果代码已经编译成库,那就太晚了。
  • I do not want to create a different function since assert is already used heavily in the project. 如果您使用 1980 年或更近的文本编辑器,这不是问题。 Refactor->rename 或 grep 和 diff/VCS 可能会为您省去很多麻烦,并且可以让您避免发明丑陋的 hack。

标签: c assert ld-preload ndebug


【解决方案1】:

在 Cygwin/Windows 和 Linux 上使用 gcc 时遇到了同样的问题。

我的解决方案是覆盖实际断言失败处理函数的(弱)定义。代码如下:

/*!
 * Overwrite the standard (weak) definition of the assert failed handling function.
 *
 * These functions are called by the assert() macro and are named differently and
 * have different signatures on different systems.
 * - On Cygwin/Windows its   __assert_func()
 * - On Linux its            __assert_fail()
 *
 * - Output format is changed to reflect the gcc error message style
 *
 * @param filename    - the filename where the error happened
 * @param line        - the line number where the error happened
 * @param assert_func - the function name where the error happened
 * @param expr        - the expression that triggered the failed assert
 */
#if defined( __CYGWIN__ )

void __assert_func( const char *filename, int line, const char *assert_func, const char *expr )

#elif defined( __linux__ )

void __assert_fail ( const char* expr, const char *filename, unsigned int line, const char *assert_func )

#else
# error "Unknown OS! Don't know how to overwrite the assert failed handling function. Follow assert() and adjust!"
#endif
{
    // gcc error message style output format:
    fprintf( stdout, "%s:%d:4: error: assertion \"%s\" failed in function %s\n",
             filename, line, expr, assert_func );

    abort();
}

【讨论】:

  • NDEBUG 被定义时,assert 将扩展为空,并且不会调用底层函数。
  • 是的。那是预期的行为。因此,该解决方案与“标准”行为一致。
  • 但是 OP 在NDEBUG 案例中询问了关于重载断言的问题:“我想创建自己的断言版本......以防在 NDEBUG 模式下调用断言”。
  • 啊,你是对的。我不明白原始问题中的 in NDEBUG 模式 。因此,如果定义了 NDEBUG,那么这个解决方案确实毫无帮助。它仅在您需要自定义断言失败消息的格式或目标(文件/套接字/...)时才有用。对不起,我的误解。
【解决方案2】:

C99 rationale 在第 113 页提供了有关如何以良好方式重新定义断言的示例:

#undef assert
#ifdef NDEBUG
#define assert(ignore) ((void)0)
#else
extern void __gripe(char *_Expr, char *_File, int _Line, const char *_Func);
#define assert(expr) \
((expr) ? (void)0 :\
 __gripe(#expr, _ _FILE_ _,_ _LINE_ _,_ _func_ _))
#endif 

我会在此代码之前包含 assert.h 以确保使用 assert.h。 另请注意,它调用了一个执行报告逻辑的函数,因此您的代码会更小。

【讨论】:

  • 注意,如果在此之后包含assert.h或cassert,它将重新定义assert为标准定义(assert.h没有包含保护)。
  • 为什么不直接将代码添加到本地assert.h 并将其路径添加到CPPFLAGS?然后它将覆盖系统assert.h
【解决方案3】:

这是一件非常简单的事情,因为assert 是一个宏。鉴于您有此代码:

#define NDEBUG
#include <assert.h>

int main( void )
{
  assert(0);

  return 0;
}

然后做:

#ifdef NDEBUG
#undef assert
#define assert(x) if(!(x)){printf("hello world!");} // whatever code you want here
#endif

请注意,这必须在 #include &lt;assert.h&gt; 之后完成。

所以如果你想将自己的定义粘贴到一个通用的头文件中,然后使用该头文件修改现有代码,那么你的头文件必须包含在assert.h之后。

my_assert.h

#include <assert.h>
#include <stdio.h>

#ifdef NDEBUG
#undef assert
#define assert(x) if(!(x)){printf("hello world!");}
#endif

main.c

#define NDEBUG    
#include <assert.h>
#include "my_assert.h"

int main( void )
{
  assert(0); // prints "hello world!"
  assert(1); // does nothing

  return 0;
}

【讨论】:

  • 作为旁注,我很确定,正式和迂腐,库宏的#undef 是UB。在这种特定情况下,它没有理由不工作。
  • 稍微挑剔,但#define assert(x) do{ if(!(x)){printf("hello world!"); }} while(0) 更好,因为分号按预期工作
  • @JeremyP 不,这是个坏主意。这样,当有人使用您的代码(例如 if(x) assert(something); else 且不带大括号)时,您将不会出现编译器错误。你永远不应该在没有大括号的情况下编写 if 语句(询问 Apple 这样做是否是个好主意)。如果您的宏强制用户编写更安全的 C,那么您的宏正在做的很好。 “do-while-zero”宏与 1980 年代的“yoda 条件”和其他此类无意义的技巧一样过时。
  • 此解决方案需要在使用assert 的每个源文件中将#include &lt;assert.h&gt; 替换为#include &lt;assert.h&gt; #include "my_assert.h"。这样,将 assert 的所有实例简单的文本替换为自定义宏可能会更容易、更健壮。
  • @Lundin 这同样不是 SO 来监管编码风格的目的。 asset() 看起来像一个函数,它在语法上应该像一个函数,并且在其他合法情况下不会导致意外的语法错误。因此,您的assert() 版本不是高质量的代码,或者至少没有达到应有的质量。
【解决方案4】:

尝试在大型代码库中覆盖 assert() 宏可能很困难。例如,假设您有如下代码:

#include <assert.h>
#include "my_assert.h"
#include "foo.h" // directly or indirectly includes <assert.h>

此后,任何对 assert() 的使用都将再次使用系统 assert() 宏,而不是您在“my_assert.h”中定义的宏(这显然是 assert 宏的 C 设计的一部分)。

有一些方法可以避免这种情况,但您必须使用一些讨厌的技巧,例如将您自己的 assert.h 标头放在系统 assert.h 之前的包含路径中,这有点容易出错且不可移植。

我建议使用与 assert 不同的命名宏,并使用正则表达式技巧或 clang-rewriter 将代码库中的各种断言宏重命名为您可以控制的断言宏。示例:

perl -p -i -e 's/\bassert\b *\( */my_assert( /;' `cat list_of_filenames`

(然后将“my_assert.h”或类似的东西添加到每个修改的文件中)

【讨论】:

  • 为什么不直接将代码添加到本地assert.h 并将其路径添加到CPPFLAGS?然后它将覆盖系统assert.h
  • 这需要信心和测试,以确保所有正在使用(或可能使用)的编译器都允许使用 -I 覆盖系统头文件。当我写这个答案时,我使用了一个基于 11 个平台变体和许多编译器的产品。我没有信心尝试我们的产品。
  • 好吧,我还没有看到一个编译器不允许通过-I(包括Visual Studio)覆盖系统头文件,我不相信有这样的编译器存在的原因...
【解决方案5】:

您可以检查NDEBUG 是否已定义,如果已定义,则打印您要打印的任何日志。

【讨论】:

  • @StoryTeller ...我同意你的观点,但对于这个问题,我觉得提供这个想法就足够了。
猜你喜欢
  • 2015-02-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-07
  • 2017-07-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多