assert简介

assert被C/C++用来判断某些条件是否成立,比如判断指针类型的大小sizeof(void*)是否大于8,或者判断malloc返回的指针是否为null。

assert的函数申明如下:

void assert( int expression );

如果expression为0,即false,assert就会把函数名,源代码文件名、当前行号发送给标准错误stderr,然后调用abort()终止执行。

什么时候使用assert

assert一般用于判断逻辑上一定为真的条件,如果条件不符合就会导致未定义行为(undefined behavior)时,就可以使用assert。

这里其实涉及到狭义约定(narrow contract)和广义约定(wide contract)的概念。所谓狭义约定,就是函数执行需要满足预定义的条件,如果不满足就会导致未定义行为,比如std::vector::operator[],如果传入的index等于或者超过vector的大小,就会导致undefined behavior。而使用广义约定的函数即使输入不符合预期,也不会出现未定义行为。比如std::vector::at(),如果输入的index超出范围,就会抛出一个异常。

如果函数使用狭义约定,那么就应当使用assert去检查输入是否合法,不合法就直接结束执行。如果使用广义约定,检查发现输入不符合预期,就应当抛出异常或者返回一个错误码或者采取其他操作,总之,采取的操作一定是预定义的,而不是未定义的。

assert只适合在debug版本中进行测试,release版本中应当禁用assert。C/C++提供了NDEBUG宏用于控制assert的行为,assert是默认生效的,如果定义了NDEBUG宏,assert就不会生效。

assert用法

int main()
{
    int num = 1;
    assert(num == 1); //true,断言成功

    num = 10;
    assert(num == 1); //false,断言失败
    return 0;
}

编译执行上述代码,输出如下:

Assertion failed: (num == 1), function main, file test.cpp, line 9.
[1]    65468 abort      ./test

可以看到第一个assert没有任何输出,断言是成功的。第二个assert断言失败了,输出了断言条件,函数名,源码文件名,assert所在行号。

static_assert

static_assert是C++11新增的静态断言关键字。assert是运行时断言,只有在执行到assert时才会进行判断。而静态断言是在编译时进行断言。所以断言的条件必须是编译时即可确定。比如断言sizeof(void*)是否为8个字节,比如断言一个MACRO是否为某个值等。而判断malloc返回的指针是否为空就不能用static_assert了。

static_assert的声明如下:

static_assert ( bool_constexpr , message )

可以看到这不是一个函数的声明,因为static_assert是关键字,不是和assert一样的库函数。bool_constexpr是指布尔常量表达式,这里有两个含义,首先是布尔表达式,这点和assert要求相同。其次需要是常量表达式,常量表达式的意思是表达式的值是可以在编译阶段确定的,比如上面说的sizeof(void*)就是一个常量表达式。

message是断言失败时打印的错误提示,这点就比assert要方便一些,可以打印一些错误提示。

我们编写一个例子测试一下static_assert。

int main()
{
    static_assert(sizeof(void*) >= 8, "void*'s size is less than 8 bytes");

    static_assert(sizeof(void*) == 7, "void*'s size is not 7 bytes");

    return 0;
}

我们把上面的代码保存为test.cpp,编译一下看看,编译输出如下:

$g++ test.cpp -o test -std=c++11    
test.cpp:5:5: error: static_assert failed due to requirement 'sizeof(void *) == 7' "void*'s size is not 7 bytes"
    static_assert(sizeof(void*) == 7, "void*'s size is not 7 bytes");
    ^             ~~~~~~~~~~~~~~~~~~
1 error generated.

可以看到在第二个static_assert处报错了,因为sizeof(void ) == 7的求值结果是false。除了打印出了源码文件名、所在行,还打印了错误提示:”void’s size is not 7 bytes”。

第一个static_assert断言sizeof(void*) >= 8,因为我是在64bit机器上编译的,默认编译为64bit的二进制,所以这个断言是成功,那么如果我们改一下编译指令,编译成32bit的程序呢?

我们给g++加上-m32参数,表示编译生成32bit的二进制,编译输出如下:

$ g++ test.cpp -o test -std=c++11 -m32
test.cpp:3:5: error: static_assert failed due to requirement 'sizeof(void *) >= 8' "void*'s size is less than 8 bytes"
    static_assert(sizeof(void*) >= 8, "void*'s size is less than 8 bytes");
    ^             ~~~~~~~~~~~~~~~~~~
test.cpp:5:5: error: static_assert failed due to requirement 'sizeof(void *) == 7' "void*'s size is not 7 bytes"
    static_assert(sizeof(void*) == 7, "void*'s size is not 7 bytes");
    ^             ~~~~~~~~~~~~~~~~~~
2 errors generated.

因为sizeof(void*)此时的值为4,所以两个断言都失败了。

static_assert有两个参数,第二个参数message可以用来打印错误提示,如果不想打印错误提示呢?在C++17之前,只能把第二个参数置为空字符串,C++17开始,message参数成为可选项,可以只填第一个参数。

在C语言中使用static_assert

C11之后可以使用Static_assert ( expression , message )

C23开始支持_Static_assert ( expression )

老版本也可能支持,但是得看编译器了=_=

如何让assert也能打印错误提示

assert并没有直接支持打印错误提示的接口,但是通过一些奇技淫巧可以实现这个功能,我们把上面的例子修改一下:

int main()
{
    int num = 1;
    assert(num == 1 && "num should be 1"); //true,断言成功

    num = 10;
    assert(num == 1 && "num should be 1"); //false,断言失败
    return 0;
}

这个利用了aseert会把断言表达式打印出来这个特性,而字符串的值是一个非0指针,所以&&右侧必然为True,不影响断言结果。

我们执行上面的代码,结果如下

$ ./test               
Assertion failed: (num == 1 && "num should be 1"), function main, file test.cpp, line 8.
[1]    79622 abort      ./test

参考文献

Assertions in C/C++ When is assert() to be used? static_assert declaration Static assertion Add custom messages in assert? - Stack Overflow