boost exception

前言

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的定义。

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_;
};
可以看到,error_info是一个模板类,我们通过指定不同的Tag类型,就可以区分不同的错误信息了。error_info_base是用做一个公共基类,存放在exception的通用容器中,这就是type erasure技术的体现。来看一下使用实例:
typedef boost::error_info<struct tag_test, int> test_error;
这里我们定义了一个test_error错误信息,其中tag_test只是一个未定义的类。根据标准,可以在模板中使用incomplete的数据结构,这样就可以避免定义一个不必要的类或结构。
注意,模板参数class Tag不要求是complete type,也就是说,我们只需要任意指定一个类型,这个类型不需要可见的定义,就像我们经常使用的前置声明一样,只是为了让生成一个唯一的模板隐式特化。

实现

区分不同的error info

因为使用了类型来区分不同的error info,而存储时,不可能放入一个统一的容器中进行管理,很显然需要借助类型擦除,但是擦除类型后,我们还是需要区分类型,因为后面还需要将不同类型的error info取出来(通过模板参数,指定error info的类型),所以自然还是需要保留error info的类型信息。显然,可以使用C++的RTTI来获得一个类的运行时类型信息。boost中也有采用这种方法,不过由于为了兼容不同的编译器(这些编译器不支持RTTI),也为了能够在RTTI关闭的情况下依然可以区分不同的类型,采用了一个自定义的type_info类。

先来看一下在没有RTTI的情况下,boost是如何实现自定义的呃RTTI的。

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_)
这里,使用一个简单的模板类,并且在其中定义一个static成员来区分不同的类型。BOOST_SP_TYPEID(T)可以根据指定的类型来获取相关的typeid,这里,是一个char变量的地址。全局变量拥有唯一的地址。

接着我们来看一下自定义的type_info包装类:

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_;
    }
};
包装类实现了标准中type_info的接口(除了bool before())。sp_typeinfo在开启RTTI的情况下就是std::type_info,而不支持的情况下是void*(根据上面的分析)。

error info container的实现

error info container用于存储不同的error info,每个boost::exception都会有一个error info container。

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的基类定义,其中diagnostic_infomation给出了所有的error_info的信息,get/set分别设置/获取error_info。可以看到这里使用boost::shared_ptr来减少内存的占用。
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;
    }
};
从实现中可以看到,container只是简单地用std::map存储error_info。细心的读者可能已经发现,有些成员是mutable,至于为什么,将会在以后进行解释。
还有一点值得注意,在diagnostic_information的实现中使用了一个临时变量,而不是直接对diagnostic_info_str_成员直接进行操作,可能是考虑到异常安全,这里用临时变量,并最后进行swap(no throw),保证了strong garantee。

设置/获取error_info

boost提供了很方便的函数向一个exception中添加error_info——使用operator<<

throw my_exception() << test_error(1);
这里我们创建了一个临时的exception,并且向其中添加了一个自定义的error_info。接着我们来看一下operator<<的实现。
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;
}
}
这是个模板函数。我们发现第一个参数是一个const参数,使用const的参数的原因是我们会像上面一样,使用创建一个临时的exception对象,修改后再抛出。标准中规定,临时对象无法绑定到non-const lvalue reference,但是可以绑定到const lvalue reference,所以我们使用const参数。而可以修改const对象的原因就是因为我们使用mutable来声明成员类型。这样用户就可以抛出一个临时对象,并同时进行修改。

NOTE: 在C++0x中,我们可以使用rvalue reference来解决这个问题。

接下来看一下如何从一个exception中获得一个error_info:

std::cout << *boost::get_error_info<test_error>(e) << std::endl;
我们使用get_error_info模板来获得相应的错误信息,该函数返回一个指向错误信息的指针。我们很容易想到,根据test_error的类型来从e中获得相应的错误信息,具体实现就不贴出了。

兼容其他类型exception

为了使其他的exception也支持error_info,我们需要调用boost::enable_error_info。这个函数有一个异常参数,并且返回一个支持error_info的exception。具体的实现如下:

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是一个用于计算返回类型的元函数,这里用到了c++的模板元编程,对于元编程不熟悉的读者可以参考《C++ Template Metaprogramming》一书。
来看一下enable_error_info_return_type如何计算返回类型的
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;
    };
}
这里,有一个元函数-enable_error_info_helper,根据T的类型来决定返回类型,通过dispatch函数,并计算dispatch的返回值的大小。如果是large_size,那么T已经是boost::exeption,否则通过创建一个新的类型继承自T和boost::exception来达到。这里继承T是为了最后用户还能直接catch T。
NOTE: 这里我们不能通过operator T()来完成无缝转换,因为c++标准规定,编译器不会在catch处考虑这种类型转换(见15.3/3)。

error_info的输出

我们可以通过boost::diagnostic_information来自动生成boost::exception的error info(std::string),当然这个函数有2个重载,其中一个针对的是std::exception,所以也可用来输出标准异常。一个典型的boost::exception输出如下:

Throw in function test_boost_exception
[const char *__cdecl boost::tag_type_name<struct tag_test>(void)] = 1
输出中包括了抛出异常的函数名,以及error_info的类型/值的信息。

其中error_info的值的输出的实现比较复杂一点,先来看一下输出过程中的调用栈:

我们来看一下调用过程中各个函数的实现。

一开始,error_info::value_as_string调用了一个to_string_stub的函数,

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);
}
接着又调用了to_string_dispatch::dispatch这个函数,其中的参数Stub是一个callable object,也就是可以是函数指针、函数对象等任何重载了operator()的对象。
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);
}
这个函数又调用了convert,这里有一个基于模板的dispatch,我们一步步来看has_to_string的实现。

has_to_string

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 };
};
is_output_streamable元函数用于判断一个是否存在用户自定义的operator<<可以用于输出error_info<T>,接着我们再来has_to_string_impl。
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())) };
};
}
可见,如果一个error_info有operator<<重载,那么has_to_string_impl::value为true,如果没有重载则再看T类型是否有to_string重载,如果有,则has_to_string_impl::value为true,否则false。接着回到dispatch函数。

dispatch

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;
    }
};
根据上面的分析,对于有operator<<和to_string重载的error_info,将会直接调用to_string,来看一下to_string的实现。
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只在T类型is_output_streamable的情况下才会被编译器启用(SFINAE),我们可以方便地通过boost::enable_if或者boost::disable_if在编译时启用/禁用某个类或者函数。

再次回到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)

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值