前言
C++的exception其实是饱受争议的一项特性,对于如何使用exception也有着诸多的观点。在其他语言中,exception来报告错误是很平常的事情,但是在C++中,用exception可没有那么简单,其中一个原因是异常安全(exception safety)的代码没有想象中那么容易编写,并且已经存在着诸多的不是异常安全的代码。还有就是许多程序员对于如何使用存在着疑问,在boost中有一篇关于如何使用异常的文章,从另一个角度剖析如何使用异常,可能会对我们如何编写异常相关的代码有些帮助——Error and Exception Handling。
准备
Type Erasure
在boost.exception库中,大量使用了type erasure技术,其中boost.Any就是对这种技术的一种实现。对该技术不熟悉的人,可以先google并结合boost.Any库进行学习。
error info
在exception中包含的信息对于用户而言是至关重要的,但是如果这个信息是固定的,可能就不太适合所有的使用情况。boost::exception被实现为可以添加任何error_info的容器,用户可以自定义任何类型,向exception中进行添加,并在catch的时候取出这个信息。为了区分不同的存储的错误信息,error_info使用tag技术进行区分。通过对error_info指定不同的tag,可以生成不同的类型。来看一下error_info的定义。
可以看到,error_info是一个模板类,我们通过指定不同的Tag类型,就可以区分不同的错误信息了。error_info_base是用做一个公共基类,存放在exception的通用容器中,这就是type erasure技术的体现。来看一下使用实例:template <class Tag,class T> class error_info : public exception_detail::error_info_base { public: typedef T value_type; error_info( value_type const & value ); ~error_info() throw(); value_type const & value() const { return value_; } private: char const * tag_typeid_name() const; std::string value_as_string() const; value_type const value_; };
这里我们定义了一个test_error错误信息,其中tag_test只是一个未定义的类。根据标准,可以在模板中使用incomplete的数据结构,这样就可以避免定义一个不必要的类或结构。typedef boost::error_info<struct tag_test, int> test_error;
注意,模板参数class Tag不要求是complete type,也就是说,我们只需要任意指定一个类型,这个类型不需要可见的定义,就像我们经常使用的前置声明一样,只是为了让生成一个唯一的模板隐式特化。
实现
区分不同的error info
因为使用了类型来区分不同的error info,而存储时,不可能放入一个统一的容器中进行管理,很显然需要借助类型擦除,但是擦除类型后,我们还是需要区分类型,因为后面还需要将不同类型的error info取出来(通过模板参数,指定error info的类型),所以自然还是需要保留error info的类型信息。显然,可以使用C++的RTTI来获得一个类的运行时类型信息。boost中也有采用这种方法,不过由于为了兼容不同的编译器(这些编译器不支持RTTI),也为了能够在RTTI关闭的情况下依然可以区分不同的类型,采用了一个自定义的type_info类。
先来看一下在没有RTTI的情况下,boost是如何实现自定义的呃RTTI的。
这里,使用一个简单的模板类,并且在其中定义一个static成员来区分不同的类型。BOOST_SP_TYPEID(T)可以根据指定的类型来获取相关的typeid,这里,是一个char变量的地址。全局变量拥有唯一的地址。namespace boost { namespace detail { typedef void* sp_typeinfo; template<class T> struct sp_typeid_ { static char v_; }; template<class T> char sp_typeid_< T >::v_; template<class T> struct sp_typeid_< T const >: sp_typeid_< T > { }; template<class T> struct sp_typeid_< T volatile >: sp_typeid_< T > { }; template<class T> struct sp_typeid_< T const volatile >: sp_typeid_< T > { }; } // namespace detail } // namespace boost #define BOOST_SP_TYPEID(T) (&boost::detail::sp_typeid_::v_)
接着我们来看一下自定义的type_info包装类:
包装类实现了标准中type_info的接口(除了bool before())。sp_typeinfo在开启RTTI的情况下就是std::type_info,而不支持的情况下是void*(根据上面的分析)。struct type_info_ { detail::sp_typeinfo type_; char const * name_; explicit type_info_( detail::sp_typeinfo type, char const * name ): type_(type), name_(name) { } friend bool operator==( type_info_ const & a, type_info_ const & b ) { return a.type_==b.type_; } friend bool operator<( type_info_ const & a, type_info_ const & b ) { return a.type_<b.type_; } char const * name() const { return name_; } };
error info container的实现
error info container用于存储不同的error info,每个boost::exception都会有一个error info container。
这个是container的基类定义,其中diagnostic_infomation给出了所有的error_info的信息,get/set分别设置/获取error_info。可以看到这里使用boost::shared_ptr来减少内存的占用。struct error_info_container { virtual char const * diagnostic_information() const = 0; virtual shared_ptr<error_info_base const> get( type_info_ const & ) const = 0; virtual void set( shared_ptr<error_info_base const> const &, type_info_ const & ) = 0; virtual void add_ref() const = 0; virtual void release() const = 0; protected: virtual ~error_info_container() throw() { } };
从实现中可以看到,container只是简单地用std::map存储error_info。细心的读者可能已经发现,有些成员是mutable,至于为什么,将会在以后进行解释。class error_info_container_impl : public error_info_container { public: error_info_container_impl(): count_(0) { } ~error_info_container_impl() throw() { } void set( shared_ptr<error_info_base const> const & x, type_info_ const & typeid_ ) { BOOST_ASSERT(x); info_[typeid_] = x; diagnostic_info_str_.clear(); } shared_ptr<error_info_base const> get( type_info_ const & ti ) const { error_info_map::const_iterator i=info_.find(ti); if ( info_.end()!=i ) { shared_ptr<error_info_base const> const & p = i->second; #ifndef BOOST_NO_RTTI BOOST_ASSERT( BOOST_EXCEPTION_DYNAMIC_TYPEID(*p)==ti ); #endif return p; } return shared_ptr<error_info_base const>(); } char const * diagnostic_information() const { if ( diagnostic_info_str_.empty() ) { std::ostringstream tmp; for ( error_info_map::const_iterator i=info_.begin(),end=info_.end(); i!=end; ++i ) { shared_ptr<error_info_base const> const & x = i->second; tmp << '[' << x->tag_typeid_name() << "] = " << x->value_as_string() << std::endl; } tmp.str().swap(diagnostic_info_str_); } return diagnostic_info_str_.c_str(); } private: friend class boost::exception; typedef std::map< type_info_, shared_ptr<error_info_base const> > error_info_map; error_info_map info_; mutable std::string diagnostic_info_str_; mutable int count_; void add_ref() const { ++count_; } void release() const { if ( !--count_ ) delete this; } };
还有一点值得注意,在diagnostic_information的实现中使用了一个临时变量,而不是直接对diagnostic_info_str_成员直接进行操作,可能是考虑到异常安全,这里用临时变量,并最后进行swap(no throw),保证了strong garantee。
设置/获取error_info
boost提供了很方便的函数向一个exception中添加error_info——使用operator<<
这里我们创建了一个临时的exception,并且向其中添加了一个自定义的error_info。接着我们来看一下operator<<的实现。throw my_exception() << test_error(1);
这是个模板函数。我们发现第一个参数是一个const参数,使用const的参数的原因是我们会像上面一样,使用创建一个临时的exception对象,修改后再抛出。标准中规定,临时对象无法绑定到non-const lvalue reference,但是可以绑定到const lvalue reference,所以我们使用const参数。而可以修改const对象的原因就是因为我们使用mutable来声明成员类型。这样用户就可以抛出一个临时对象,并同时进行修改。template <class E,class Tag,class T> inline E const & operator<<( E const & x, error_info<Tag,T> const & v ) { typedef error_info<Tag,T> error_info_tag_t; shared_ptr<error_info_tag_t> p( new error_info_tag_t(v) ); exception_detail::error_info_container * c; if ( !(c=x.data_.get()) ) x.data_.adopt(c=new exception_detail::error_info_container_impl); c->set(p,BOOST_EXCEPTION_STATIC_TYPEID(error_info_tag_t)); return x; } }
NOTE: 在C++0x中,我们可以使用rvalue reference来解决这个问题。
接下来看一下如何从一个exception中获得一个error_info:
我们使用get_error_info模板来获得相应的错误信息,该函数返回一个指向错误信息的指针。我们很容易想到,根据test_error的类型来从e中获得相应的错误信息,具体实现就不贴出了。std::cout << *boost::get_error_info<test_error>(e) << std::endl;
兼容其他类型exception
为了使其他的exception也支持error_info,我们需要调用boost::enable_error_info。这个函数有一个异常参数,并且返回一个支持error_info的exception。具体的实现如下:
这里enable_error_info_return_type是一个用于计算返回类型的元函数,这里用到了c++的模板元编程,对于元编程不熟悉的读者可以参考《C++ Template Metaprogramming》一书。template <class T> inline typename exception_detail::enable_error_info_return_type<T>::type enable_error_info( T const & x ) { typedef typename exception_detail::enable_error_info_return_type<T>::type rt; return rt(x); }
来看一下enable_error_info_return_type如何计算返回类型的
这里,有一个元函数-enable_error_info_helper,根据T的类型来决定返回类型,通过dispatch函数,并计算dispatch的返回值的大小。如果是large_size,那么T已经是boost::exeption,否则通过创建一个新的类型继承自T和boost::exception来达到。这里继承T是为了最后用户还能直接catch T。namespace exception_detail { template <class T> struct error_info_injector: public T, public exception { explicit error_info_injector( T const & x ): T(x) { } ~error_info_injector() throw() { } }; struct large_size { char c[256]; }; large_size dispatch( exception * ); struct small_size { }; small_size dispatch( void * ); template <class,int> struct enable_error_info_helper; template <class T> struct enable_error_info_helper<T,sizeof(large_size)> { typedef T type; }; template <class T> struct enable_error_info_helper<T,sizeof(small_size)> { typedef error_info_injector<T> type; }; template <class T> struct enable_error_info_return_type { typedef typename enable_error_info_helper<T,sizeof(dispatch((T*)0))>::type type; }; }
NOTE: 这里我们不能通过operator T()来完成无缝转换,因为c++标准规定,编译器不会在catch处考虑这种类型转换(见15.3/3)。
error_info的输出
我们可以通过boost::diagnostic_information来自动生成boost::exception的error info(std::string),当然这个函数有2个重载,其中一个针对的是std::exception,所以也可用来输出标准异常。一个典型的boost::exception输出如下:
输出中包括了抛出异常的函数名,以及error_info的类型/值的信息。Throw in function test_boost_exception [const char *__cdecl boost::tag_type_name<struct tag_test>(void)] = 1
其中error_info的值的输出的实现比较复杂一点,先来看一下输出过程中的调用栈:
我们来看一下调用过程中各个函数的实现。
一开始,error_info::value_as_string调用了一个to_string_stub的函数,
接着又调用了to_string_dispatch::dispatch这个函数,其中的参数Stub是一个callable object,也就是可以是函数指针、函数对象等任何重载了operator()的对象。template <class T> inline std::string to_string_stub( T const & x ) { return exception_detail::to_string_dispatch::dispatch(x,&exception_detail::string_stub_dump<T>); } template <class T,class Stub> inline std::string to_string_stub( T const & x, Stub s ) { return exception_detail::to_string_dispatch::dispatch(x,s); }
这个函数又调用了convert,这里有一个基于模板的dispatch,我们一步步来看has_to_string的实现。template <class T,class Stub> inline std::string dispatch( T const & x, Stub s ) { return to_string_dispatcher<has_to_string<T>::value>::convert(x,s); }
has_to_string
is_output_streamable元函数用于判断一个是否存在用户自定义的operator<<可以用于输出error_info<T>,接着我们再来has_to_string_impl。namespace to_string_detail { template <class T,class CharT,class Traits> char operator<<( std::basic_ostream<CharT,Traits> &, T const & ); template <class T,class CharT,class Traits> struct is_output_streamable_impl { static std::basic_ostream<CharT,Traits> & f(); static T const & g(); enum e { value=1!=(sizeof(f()<<g())) }; }; } template <class T, class CharT=char, class Traits=std::char_traits<CharT> > struct is_output_streamable { enum e { value=to_string_detail::is_output_streamable_impl<T,CharT,Traits>::value }; }; template <class T> struct has_to_string { enum e { value=to_string_detail::has_to_string_impl<T,is_output_streamable<T>::value>::value }; };
可见,如果一个error_info有operator<<重载,那么has_to_string_impl::value为true,如果没有重载则再看T类型是否有to_string重载,如果有,则has_to_string_impl::value为true,否则false。接着回到dispatch函数。namespace to_string_detail { template <class T> typename disable_if<is_output_streamable<T>,char>::type to_string( T const & ); template <class,bool IsOutputStreamable> struct has_to_string_impl; template <class T> struct has_to_string_impl<T,true> { enum e { value=1 }; }; template <class T> struct has_to_string_impl<T,false> { static T const & f(); enum e { value=1!=sizeof(to_string(f())) }; }; }
dispatch
根据上面的分析,对于有operator<<和to_string重载的error_info,将会直接调用to_string,来看一下to_string的实现。template <bool ToStringAvailable> struct to_string_dispatcher { template <class T,class Stub> static std::string convert( T const & x, Stub ) { return to_string(x); } }; template <> struct to_string_dispatcher<false> { template <class T,class Stub> static std::string convert( T const & x, Stub s ) { return s(x); } template <class T> static std::string convert( T const & x, std::string s ) { return s; } template <class T> static std::string convert( T const & x, char const * s ) { BOOST_ASSERT(s!=0); return s; } };
第一个to_string只在T类型is_output_streamable的情况下才会被编译器启用(SFINAE),我们可以方便地通过boost::enable_if或者boost::disable_if在编译时启用/禁用某个类或者函数。template <class T> inline typename enable_if<is_output_streamable<T>,std::string>::type to_string( T const & x ) { std::ostringstream out; out << x; return out.str(); } template <class T,class U> inline std::string to_string( std::pair<T,U> const & x ) { return std::string("(") + to_string(x.first) + ',' + to_string(x.second) + ')'; } inline std::string to_string( std::exception const & x ) { return x.what(); }
再次回到to_string_stub
在该函数中,传入了一个参数string_stub_dump函数,这个函数会调用object_hex_dump,由它dump出对象的起始的若干字节,默认是16个字节。
至此,关键的实现已经呈现在大家面前。template <class T> inline std::string object_hex_dump( T const & x, size_t max_size=16 ) { std::ostringstream s; s << "type: " << type_name<T>() << ", size: " << sizeof(T) << ", dump: "; size_t n=sizeof(T)>max_size?max_size:sizeof(T); s.fill('0'); s.width(2); unsigned char const * b=reinterpret_cast<unsigned char const *>(&x); s << std::setw(2) << std::hex << (unsigned int)*b; for ( unsigned char const * e=b+n; ++b!=e; ) s << " " << std::setw(2) << std::hex << (unsigned int)*b; return s.str(); }
最后我们来整理一下如何转换boost.exception对象的
检查对于error_info
if has_to_string
if is_output_streamable
call operator<<(os, error_info)
else
call to_string
else
call stub(error_info)