exception
异常是C++错误处理的重要机制,它改变了传统的使用错误返回值的处理模式,简化了函数的接口和调用代码,有助于编写整洁、优雅、健壮的程序。C++标准定义了标准异常类std::exception及一系列子类,是整个C++语言错误处理的基础。
boost.exception库针对标准库中异常类的缺陷进行了强化,提供<<操作符重载,可以向异常传入任意数据,有助于增加异常的信息和表达力,其中的部分特性已经被收入C++11标准。
exception位于名字空间boost,为使用exception,需要包含头文件<boost/exception/all.hpp>,即:
#include<boost/exception/all.hpp>
using namespace boost;
标准库中的异常
为了使用boost.exception,我们需要先了解C++标准规定的异常体系。
C++标准中定义了一个异常基类std::exception和 try/catch/throw异常处理机制,std::exception又派生出若干子类,用以描述不同种类的异常,如 bad_alloc、bad_cast、out_of_range等,共同构建了C++异常处理框架。
C++允许任何类型作为异常抛出,但在std::exception 出现后,我们应该尽量使用它,因为std::exception提供了一个很有用的成员函数what() ,可以返回异常所携带的信息,这比简单地抛出一个整数错误值或者字符串更好更安全。
如果std::exception及其子类不能满足程序对异常处理的要求,则我们也可以继承它,为它添加更多的异常诊断信息。例如,想要为异常增加错误码信息可以这样:
class my_exception : public std::logic_error //继承标准库异常类
{
private:
int err_no; //错误码信息
public:
my_exception(const char* msg, int err) : //构造函数
std::logic_error(msg), err_no(err) {
} //初始化父类和错误码信息
int get_err_no() //获取自定义的错误码信息
{
return err_no; }
};
但当系统中需要很多不同种类的异常时,这种实现方法很快就成为了程序员的沉重负担——为了容纳不同的信息需要编写大量极其相似的代码。
而且这种解法还存在一个问题:很多时候当发生异常时不能获得有关异常的完全诊断信息,而标准库的异常类一旦被抛出,它就成为了一个“死”对象,程序失去了对它的控制能力,只能使用它或者再抛出一个新的异常。
boost.exception针对这些缺陷定义了新的异常类boost::exception,完善了C++标准的异常处理机制。
接下来的讨论在说到exception时,如果不特别指明,均是指boost::exception,标准库的异常使用std::exception的形式。
类摘要
exception库提供两个类:exception和error_info,它们是exception库的基础。
class exception
{
protected:
exception();
exception(exception const & x);
virtual ~exception();
private :
template <class E, class Tag, class T>
friend E const & operator<<(E const &, error_info<Tag,T> const &);
};
typename value_type* get_error_info(E & x);
exception类几乎没有公开的成员函数(但有大量用于内部实现的私有函数和变量),被保护的构造函数表明了它的设计意图:它是一个抽象类,除了它的子类,任何人都不能创建或者销毁它,这保证了exception不会被误用。
exception的重要能力在于其友元操作符<<,可以存储error_info对象的信息,存入的信息可以用自由函数get_error_info<>()
随时再取出来。这个函数返回一个存储数据的指针,如果exception里没有这种类型的信息则返回空指针。
exception特意没有从std::exception继承,因为现实中已存的大量代码已经有很多std::exception 的子类,而如果 exception 也是 std::exception 的子类,则对exception再使用继承可能会引发“钻石型”多重继承问题。
template <class Tag, class T>
class error_info
{
public:
typedef T value_type;
error_info(value_type const &v);
value_type & value();
};
error_info提供了向异常类型添加信息的通用解法。第一个模板类型参数Tag是一个标记。它通常(最好)是一个空类,仅用来标记error_info类,使它在模板实例化时生成不同的类型。第二个模板类型参数T是真正存储的信息数据,可以用成员函数value()访问。
向异常传递信息
exception和 error_info被设计为配合std::exception一起工作,自定义的异常类可以安全地从exception和 std::exception多重继承,从而获得两者的能力。
因为exception被定义为抽象类,因此我们的程序必须定义它的子类才能使用它,如前所述,exception必须使用虚继承的方式。通常,继承完成后自定义异常类的实现也就结束了,不需要“画蛇添足”地向它增加成员变量或者成员函数,这些工作都已经由 exception完成了。例如:
struct my_exception : //自定义异常类
virtual std::exception, //虚继承,struct默认public继承
virtual boost::exception //虚继承,struct默认public继承
{
}; //空实现,不需要实现代码
接下来我们需要定义异常需要存储的信息——使用模板类error_info。用一个struct作为第一个模板参数来标记信息类型,再用第二个模板参数指定信息的数据类型。由于error_info<>
的类型定义较长,为了使用方便起见,通常需要使用typedef。
下面的代码使用error_info定义了两个存储int和 string的信息类:
int main()
try //function-try块
{
try
{
throw my_exception() << err_no(10); //抛出异常,存储错误码
}
catch (my_exception& e) //捕获异常,使用引用形式
{
cout << *get_error_info<err_no>(e) << endl;
cout << e.what() << endl;
e << err_str("other info"); //向异常追加信息
throw; //再次抛出异常
}
}
catch (my_exception& e) //function-try的捕获代码
{
cout << *get_error_info<err_str>(e) << endl; //获得异常信息
}
代码里的注释已经解释了很多东西,我们再来看一下。
程序首先定义了一个异常类my_exception,然后使用typedef定义了两种异常信息:err_no 和 err_str,用int 和 string 分别存储错误码和错误信息。main()函数使用function-try块来捕获异常,它把整个函数体都包含在try块中,可以更好地把异常处理代码与正常流程代码分离。
throw my_exception()语句创建了一个my_exception异常类的临时对象,并立刻使用<<向它传递了err_no对象,存入错误码10。随后,异常被catch 块捕获,自由函数get_error_info<err_no>(e)
可以获得异常内部保存的信息值的指针,所以需要用解引用操作符*访问。
异常还可以被追加信息,同样使用操作符<<,最后在function-try的catch块部分,异常被最终处理,程序结束。
错误信息类
通过例子我们基本了解了exception的用法,可以看到exception的使用是相当清晰简单的,同时又提供了灵活强大的功能,使异常类有了更多的用途。
由于从exception派生的异常类定义非常简单,没有实现代码,因此,可以很容易地建立起一个适合自己程序的、精细完整的异常类体系,使每个异常类只对应一种错误类型。只要都使用虚继承,类体系可以任意复杂,充分表达错误的含义。
处理异常的另一个重要工作是定义错误信息类型,基本方法是使用typedef来具体化error_info模板类。这通常比较麻烦,特别是有大量信息类型的时候。因此 exception库特意提供若干预先定义好的错误信息类,如同标准库定义的logic_err等类型,使程序员用起来更轻松:
typedef error_info<...> errinfo_api_function;
typedef error_info<...> errinfo_at_iine;
typedef error_info<...> errinfo_errno;
typedef error_info<...> errinfo_file_handle;
typedef error_info<...> errinfo_file_name;
typedef error_info<...> errinfo_file_open_mode;
typedef error_info<...> errinfo_type_info_name;
它们可以用于常见的调用API、行号、错误代码、文件 handle、文件名等错误信息的处理。例如:
try
{
throw my_exception() << errinfo_api_function("call api")
<< errinfo_errno(101);
}
catch (boost::exception& e)
{
cout << *get_error_info<errinfo_api_function>(e);
cout << *get_error_info<errinfo_errno>(e);
}
另外,exception库还提供三个预定义错误信息类型,但命名规则略有不同:
typedef error_info<...> throw_function;
typedef error_info<...> throw_file;
typedef error_info<...> throw_line;
这三个错误信息类型主要用于存储源代码的信息,配合宏 BOOST_CURRENT_FUNCTION、_FILE_和_LINE_使用,可以获得调用函数名、源文件名和源代码行号。
但如果这些预定义类不能满足要求,我们还要再使用typedef。为解决这个不大不小的麻烦,我们可以自定义一个辅助工具宏DEFINE_ERROR_INPO,它可以方便快捷地实现error_info的定义:
#define DEFINE_ERROR_INFO(type, name) \
typedef boost::error_info<struct tag_##name, type> name
宏DEFINE_ERROR_INFO接受两个参数,type是它要存储的类型,name 是所需要的错误信息类型名,使用预处理命令##创建了error_info所需要的标签类。它的使用方法很简单,就像是声明一个变量:
DEFINE_ERROR_INFO(int, err_no);
在宏展开后它相当于:
typedef boost::error_info<struct tag_err_no, int> err_no;
包装标准异常
exception库提供一个模板函数enable_error_info(T &e),其中T是标准异常类或者其他自定义类型。它可以包装类型T,产生一个从boost::exception和T派生的类,从而在不修改原异常处理体系的前提下获得 boost::exception的所有好处。如果类型T已经是boost::exception 的子类,那么enable_error_info将返回e的一个拷贝。
enable_error_info()通常用在程序中已经存在异常类的场合,对这些异常类的修改很困难甚至不可能(比如已经编译成库)。这时候enable_error_info()就可以包装原有的异常类,从而很容易地在不变动任何已有代码的基础上把 boost::exception集成到原有异常体系中。
示范enable_error_info()用法的代码如下:
struct my_err {
}; //某个自定义的异常库,未使用boost::exception
int main()
{
try
{
//使用enable_error_info包装自定义异常
throw enable_error_info(my_err()) << errinfo_errno(10);
}
catch (boost::exception& e) //这里必须使用boost::exception来捕获
{
cout << *get_error_info<errinfo_errno>(e) << endl;
}
}
注意代码中catch 的用法,enable_error_info()返回的对象是boost::exception和 my_err的子类,catch的参数可以是这两者中的任意一个,但如果要使用boost::exception所存储的信息,就必须用boost::exception来捕获异常。
enable_error_info()也可以包装标准库的异常:
throw enable_error_info(std::runtime_error("runtime"))
<< errinfo_at_line(_LINE_); //包装标准异常
使用函数抛出异常
exception 库为异常增加了许多新的功能,带来了很多好处,但由于各种各样的原因,程序中的异常类并不能总是从boost::exception继承,必须使用enable_error_info()来包装。
exception 库在头文件<boost/throw_exception.hpp>里提供throw_exception()函数来简化enable_error_info()的调用,它可以代替原始的throw语句来抛出异常,会自动使用enable_error_info()来包装异常对象,而且支持线程安全,比直接使用throw更好,相当于:
throw (boost::enable_error_info(e))
从而确保抛出的异常是boost::exception的子类,可以追加异常信息。例如:
throw_exception (std::runtime_error("runtime"));
在throw_exception()的基础上 exception 库又提供了一个非常有用的宏BOOST_THROW_EXCEPTION,它调用了boost::throw_exception()和enable_error_info(),因而可以接受任意的异常类型,同时又使用throw_function、 throw_file 和throw_line自动向异常添加了发生异常的函数名、文件名和行号等信息。
但需要注意一点,为了保证与配置宏BOOST_NO_EXCEPTIONS兼容,throw_exception()函数和BOOST_THROW_EXCEPTION宏都要求参数e必须是std::exception的子类。
对于新写的异常类来说这通常不是问题,因为它们总是从std::exception和 boost::exception继承,但假如旧代码中有非 std::exception 的派生异常就无法使用,这在一定程度上限制了throw_exception()和 BOOST_NO_EXCEPTIONS的应用范围。
如果确保在程序中总使用boost::exception,不会去定义配置宏BO0ST_NO_EXCEPTIONS(这应该是大多数情况),那么可以修改Boost源代码,在<boost/throw_exception.hpp>里注释掉throw_exception_assert_compatibility(e)这条语句,以取消这个限制。
获得更多的调试信息
exception提供了方便存储信息的能力,可以向它添加任意数量的信息,但当异常对象被用operator<<多次追加数据时,会导致它存储有大量的信息(BOOST_THROW_EXCEPTION就是个很好的例子)。如果还是采用自由函数 get_error_info来逐项检索的话可能会很麻烦甚至不可能,这时我们需要另外一个函数:diagnostic_information()。
diagnostic_information()可以输出异常包含的所有信息,如果异常是由宏BOOST_THROW_EXCEPTION抛出的,则可能相当多并且不是用户友好的,但对于程序开发者可以提供很好的诊断错误的信息。
示范 BOOST_THROW_EXCEPTION和diagnostic_information()用法的代码如下:
struct my_err() //自定义的异常类
int main()
{
try
{
//使用enable_error_info包装自定义异常
throw enable_error_info(my_err())
<< errinfo_errno(101)
<< errinfo_api_function("fopen");
}
catch (boost::exception& e)
{
cout << diagnostic_information(e) << endl;
}
try
{
BOOST_THROW_EXCEPTION(std::logic_error("logic")); //必须是标准异常
}
catch (boost::exception& e)
{
cout << diagnostic_information(e) << endl;
}
}
运行结果显示了普通的throw语句与BOOST_THROW_EXCEPTION的不同,后者可以显示出更多对调试有用的信息。
exception 库里还有一个更方便的函数current_exception_diagnostic_information(),它只能在catch块内部使用,以std::string返回异常的诊断字符串,这兔去了指定异常参数的小麻烦。
对异常信息打包
exception支持使用boost::tuple对异常信息进行组合打包,当异常信息类型很多又经常成组出现时可以简化抛出异常的编写。例如:
//tuple类型定义
typedef tuple<errinfo_api_function, errinfo_errno> err_group;
try
{
//使用enable_error_info包装自定义异常
throw enable_error_info(std::out_of_range("out"))
<< err_group("syslogd", 874);
}
catch (boost::exception&)
{
cout << current_exception_diagnostic_information() << endl;
}
类型转换
模板函数 current_exception_cast<E>()
提供类似标准库的转型操作,它类似于current_exception_diagnostic_information(),只能在 catch 块内部使用,可以把异常对象转型为指向E类型的指针,如果异常对象无法转换成E*,则返回空指针。
例如下面的代码把 boost::exception转型成为了std::exception,前提是异常必须是std::exception的子类:
catch (boost::exception&)
{
cout << current_exception_cast<std::exception>()->what();
}
线程间传递异常
exception库支持在线程间传递异常,这需要使用boost::exception的clone能力。使用enable_current_exception()包装异常对象或者使用 throw_exception()都能够包装异常对象使之可以被clone。
当发生异常时,线程在需要catch 块调用函数current_exception()得到当前异常对象的指针exception_ptr对象,它指向异常对象的拷贝,是线程安全的,可以被多个线程同时拥有并发修改。rethrow_exception()可以重新抛出异常。
示范在线程中处理异常的代码如下:
void thread_work() //线程工作函数
{
throw_exception(std::exception("test"));
}
int main()
{
try
{
thread_work();
}
catch(...)
{
exception_ptr e = current_exception();
cout << current_exception_diagnostic_information();
}
}
boost::exception的这个用法已经被收入C++11标准
代码示例
#include <iostream>
using namespace std;
#include <boost/exception/all.hpp>
using namespace boost;
//
class my_exception_test : public std::logic_error
{
private:
int err_no;
public:
my_exception_test(const char* msg, int err) :
std::logic_error(msg), err_no(err) {
}
int get_err_no()
{
return err_no;
}
};
struct my_exception :
virtual std::exception,
virtual boost::exception
{
};
typedef boost::error_info<struct tag_err_no, int> err_no;
typedef boost::error_info<struct tag_err_str, string> err_str;
//
void case1()
try
{
try
{
throw my_exception() << err_no(10);
}
catch (my_exception& e)
{
cout << *get_error_info<err_no>(e) << endl;
cout << e.what() << endl;
e << err_str("other info");
throw;
}
}
catch (my_exception& e)
{
cout << *get_error_info<err_str>(e) << endl;
}
//
#define DEFINE_ERROR_INFO(type, name) \
typedef boost::error_info<struct tag_##name, type> name
void case2()
try
{
throw my_exception() << errinfo_api_function("call api")
<< errinfo_errno(101);
}
catch (boost::exception& e)
{
cout << *get_error_info<errinfo_api_function>(e);
cout << *get_error_info<errinfo_errno>(e);
cout << endl;
}
//
struct my_err {
};
void case3()
{
try
{
throw enable_error_info(my_err()) << errinfo_errno(10);
throw enable_error_info(std::runtime_error("runtime"))
<< errinfo_at_line(__LINE__);
}
catch (boost::exception& e)
{
cout << *get_error_info<errinfo_errno>(e) << endl;
}
}
//
#include <boost/throw_exception.hpp>
void case4()
{
try
{
throw enable_error_info(my_err())
<< errinfo_errno(101)
<< errinfo_api_function("fopen");
}
catch (boost::exception& e)
{
cout << diagnostic_information(e) << endl;
}
try
{
BOOST_THROW_EXCEPTION(std::logic_error("logic"));
}
catch (boost::exception& e)
{
cout << diagnostic_information(e) << endl;
}
}
int main()
{
case1();
case2();
case3();
case4();
}