Boost开发指南-4.9exception

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();
}

猜你喜欢

转载自blog.csdn.net/qq_36314864/article/details/132491947