C语言在语言层面上没有“异常”的概念,也不提类似C++那样能抛出异常的手段。但是,如果在C++中调用C的函数,不能就此完全忽略异常检查。特别是自从C++11开始,不能钦定一个C函数调用是noexcept
。至于为什么,下面说几个蛋疼的案例。
我们知道,通过extern "C"
,C++编译器完全可以导出一个C函数接口,尽管函数内部可能包含C++的调用。如果这些C++代码会抛异常,那么最终的C接口运行时也会抛出异常。代码如下:
libfoo.h
void foo();
libfoo.cpp
#include <stdexcept>
extern "C" {
#include "libfoo.h"
void foo()
{
throw std::logic_error("foo logic error");
}
}
testfoo.cpp
#include <iostream>
#include <stdexcept>
extern "C" {
#include "libfoo.h"
}
int main()
{
try {
foo();
} catch (std::logic_error e) {
std::cout << e.what();
}
return 0;
}
首先用g++ -shared -fPIC -o libfoo.so libfoo.cpp
编译出动态库libfoo.so
,然后g++ -o testfoo testfoo.cpp -L./ -lfoo
,最后执行./testfoo
:
foo logic error
可见logic_error
被成功捕获,尽管从testfoo.cpp
角度看,foo();
完全是个C函数调用。
如果说前面的是因为在C函数实现过程中掺杂了C++的成分,那么一个用纯C实现的函数就不抛异常了吗?如果参数包含函数指针,就不确定了。看下面代码:
testbar.cpp
#include <iostream>
#include <stdexcept>
extern "C" {
#include <stdlib.h>
}
int cmp(const void *l, const void *r)
{
throw std::logic_error("bar logic error");
return *(const int *)(l) - *(const int *)(r);
}
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int k = 3;
const void *res;
try {
res = bsearch(&k, arr, 5, sizeof(k), cmp);
} catch (std::logic_error e) {
std::cout << e.what() << std::endl;
}
return 0;
}
对C标准库的bsearch
的调用传入了一个抛异常的函数指针,最终bsearch
也会抛异常。编译执行后,输出bar logic error
。
上面两种情况的异常根本上都是C++代码抛出的,还有一种情况则不同。直接看代码:
testbaz.cpp
#include <pthread.h>
#include <iostream>
void* baz(void* arg)
{
std::cout << __func__ << std::endl;
try {
pthread_exit(NULL);
} catch (...) {
std::cout << "exception catched." << std::endl;
throw;
}
return NULL;
}
int main()
{
pthread_t t;
pthread_create(&t, NULL, baz, NULL);
pthread_join(t, NULL);
std::cout << "finished." << std::endl;
return 0;
}
编译执行以后,输出:
baz
exception catched.
finished.
为什么调用pthread_exit
会产生异常?按照posix标准来讲,pthread_exit
的作用是强制线程退出,这个函数是没有返回的,所以需要它来手动清理调用栈,其行为是一个forced_unwind
,类似于一个异常。在libstdc++
的实现中,这是一个abi::__forced_unwind
类型(man 3 abi::__forced_unwind
有真相)。注意这个异常catch到后必须重新抛出,否则无法正常完成栈清理。
同时要注意不能在析构函数中调用pthread_exit
,因为析构函数不允许抛异常。
其实pthread_cancel
也会有相同的行为。以下来自glibc-2.28
源码:
nptl/pthread_cancel.c
int
__pthread_cancel (pthread_t th)
{
......
pid_t pid = __getpid ();
INTERNAL_SYSCALL_DECL (err);
int val = INTERNAL_SYSCALL_CALL (tgkill, err, pid, pd->tid,
SIGCANCEL);
......
}
发了一个SIGCANCEL
信号。信号处理注册在:
nptl/nptl-init.c
static void
sigcancel_handler (int sig, siginfo_t *si, void *ctx)
{
......
/* Set the return value. */
THREAD_SETMEM (self, result, PTHREAD_CANCELED);
/* Make sure asynchronous cancellation is still enabled. */
if ((newval & CANCELTYPE_BITMASK) != 0)
/* Run the registered destructors and terminate the thread. */
__do_cancel ();
break;
}
......
}
void
__pthread_initialize_minimal_internal (void)
{
......
struct sigaction sa;
__sigemptyset (&sa.sa_mask);
# ifdef SIGCANCEL
/* Install the cancellation signal handler. If for some reason we
cannot install the handler we do not abort. Maybe we should, but
it is only asynchronous cancellation which is affected. */
sa.sa_sigaction = sigcancel_handler;
sa.sa_flags = SA_SIGINFO;
(void) __libc_sigaction (SIGCANCEL, &sa, NULL);
......
}
再看pthread_exit
:
nptl/pthread_exit.c
void
__pthread_exit (void *value)
{
THREAD_SETMEM (THREAD_SELF, result, value);
__do_cancel ();
}
最终都是调用的__do_cancel()
:
nptl/pthreadP.h
static inline void
__attribute ((noreturn, always_inline))
__do_cancel (void)
{
struct pthread *self = THREAD_SELF;
/* Make sure we get no more cancellations. */
THREAD_ATOMIC_BIT_SET (self, cancelhandling, EXITING_BIT);
__pthread_unwind ((__pthread_unwind_buf_t *)
THREAD_GETMEM (self, cleanup_jmp_buf));
}
nptl/unwind.c
void
__cleanup_fct_attribute __attribute ((noreturn))
__pthread_unwind (__pthread_unwind_buf_t *buf)
{
struct pthread_unwind_buf *ibuf = (struct pthread_unwind_buf *) buf;
struct pthread *self = THREAD_SELF;
/* This is not a catchable exception, so don't provide any details about
the exception type. We do need to initialize the field though. */
THREAD_SETMEM (self, exc.exception_class, 0);
THREAD_SETMEM (self, exc.exception_cleanup, &unwind_cleanup);
_Unwind_ForcedUnwind (&self->exc, unwind_stop, ibuf);
/* NOTREACHED */
/* We better do not get here. */
abort ();
}
旧版本中,__pthread_unwind
是通过setjmp/longjmp
实现的;在这一版glibc中,_Unwind_ForcedUnwind
最终会调用libgcc
中的_Unwind_ForcedUnwind
,见sysdeps/nptl/unwind-forcedunwind.c
,libgcc
最终会根据不同的异常规则(SEH、sjlj)等生成不同的代码,比较复杂。