本章将详细讲述Boost库中提供的数个有用的小工具(不全是类,也有宏。是的,宏也可以很有用)。说它们小,是因为它们实现的功能比较单纯,代码也都比较简单(但也有例外),在实际产品代码中也往往处于不起眼的角落之中。
但这些Boost组件都非常非常有用。
正是因为小,它们几乎在程序中无处不在,就像轴承里的滚珠或者引擎里的润滑油,能够使程序运转得更加良好,更加有效率。有了它们,会使你的编程工作更加轻松愉快。你是否曾经反复地实现一个不可拷贝的类?是否曾经为编写操作符重载而不停地敲打键盘?是否曾经为验证性测试而重复输入大量数据?是否……而这些实现仅仅是由于少量的代码不同。很多开发团队都曾经编写过大量用于自己项目的实用工具类,但因为接口不标准、文档不齐全等各种原因导致难以在更大的范围里复用。
下面的Boost组件会把程序员从这种机械并且乏味的代码复制粘贴活动中彻底解放出来。
4.1 noncopyable
noneable允许程序轻松实现一个禁止拷贝的类。
noncopyable位于名字空间boost,需要包含头文件
① noncopyable的真正实现源码位于
。
4.4.1 原理
在C++中定义一个类时,如果不明确定义拷贝构造函数和拷贝赋值操作符,编译器会为我们自动生成这两个函数。
例如:
class empty_class {};
这样一个简单的“空”类①,编译器在处理时会“默默地”为它增加拷贝构造函数和拷贝赋值操作符,真实代码类似于:
class empty_class{public: // 构造和析构暂时忽略empty_class (const empty_class &){...} // 拷贝构造函数empty_class & operator=(const empty_class &){...} // 拷贝赋值};
一般情况下这是有用的,比如可以自动支持swap()、符合容器的拷贝语义、可以放入标准容器处理,但有的时候我们不需要类的拷贝语义,希望禁止拷贝类的实例。
这是一个很经典的C++惯用法,原理很好理解,只需要私有化拷贝构造函数和拷贝赋值操作符即可,手写代码也很简单(3.2节的scoped_ptr就使用了这个惯用法),例如:
class do_not_copy{private:do_not_copy (const do_not_copy &); // 私有化,声明即可,不需要实现代码void operator=(const do_not_copy &); // 私有化,声明即可,不需要实现代码};
但如果程序中有大量这样的类,重复这样的代码是相当乏味的,而且代码出现的次数越多越容易增大手写出错的几率。虽然也可以用带参数的宏来减少重复,但解决方案不够优雅。
① Boost在头文件
定义了一个有用的空类 blank,支持比较和流输出。
4.1.2 用法
noncopyable为实现不可拷贝的类提供了简单清晰的解决方案:从boost::noncopyable派生即可。
使用noncopyable,上面的例子可简化为:
class do_not_copy: boost::noncopyable // 注意这里,使用默认的私有继承是允许的{...};
我们也可以显式写出private或者public修饰词,但效果是相同的。因此直接这样写少输入了一些代码,也更清晰,并且表明了HAS-A关系(而不是IS-A)。
如果有其他人误写了代码(很可能是没有仔细阅读接口文档),企图拷贝构造或者赋值这个对象,那么将不能通过编译器的审查:
do_not_copy d1; // 一个不可拷贝对象do_not_copy d2(d1); // 企图拷贝构造,编译出错!do_not_copy d3; // 另一个不可拷贝对象d3 = d1; // 企图拷贝赋值,编译出错
使用 GCC 编译会报出类似下面的错误提示:
error: use of deleted function 'boost::noncopyable_ ...'
这条错误信息明确地告诉我们:类使用boost::noncopyable禁用(delete)了拷贝构造,无法调用拷贝构造函数。
只要有可能,就使用boost::noncopyable,它明确无误地表达了类设计者的意图,对用户更加友好,而且与其他 Boost 库也配合得很好。
4.1.3 实现
noncopyable的实现原理很简单,代码很少:
class noncopyable
{
protected:
noncopyable(); // 私有化的构造和析构是保护的,可继承
~noncopyable();
private:
noncopyable(const noncopyable&); // 私有化拷贝构造和拷贝赋值
const noncopyable& operator=(const noncopyable&);
};
因此,当我们的自定义类是noncopyable的子类时就会自动私有化父类noncopyable的拷贝构造函数,从而禁止用户从外部访问拷贝构造函数和拷贝赋值函数。
如果使用C++标准的default和delete关键字,则noncopyable可以更清晰地实现如下:
class noncopyable
{
protected:
noncopyable() = default; // 默认的构造和析构是保护的
~noncopyable() = default; // 使用默认实现
// 使用delete关键字禁用拷贝构造和拷贝赋值
noncopyable( const noncopyable& ) = delete;
const noncopyable& operator=( const noncopyable& ) = delete;
};
4.2 ignore_unused
编写代码的过程中有时会出现一些暂时用不到但又必须保留的变量,GCC等编译器会对此发出警告,使用“-Wunused”可以关闭这些警告消息,不过这也有可能导致潜在的隐患。古老的办法是使用“(void)var”的形式来“使用”一下变量,但这种方法含义不明确,不利于维护。
Boost程序库的ignore_unused组件就这个问题给出了更好的解决方案。它原本是proto库里的一个小工具,因为在很多其他组件中都被使用,所以在增强了功能后被“扶正”。
ignore_unused位于名字空间boost,需要包含头文件
4.2.1 基本用法
ignore_unused的实现非常地简单,几乎什么也没有做:
template <typename... Ts>
inline void ignore_unused(Ts const& ...) {}
ignore_unused使用可变参数模板,可以支持任意数量、任意类型的变量,把它们作为函数的参数“使用”了一下,“骗”过了编译器,达到了与“(void)var”完全相同的效果。但它的命名更清晰,写法也更简单,而且由于是inline函数,完全没有运行时的效率损失。
假设我们有如下的一个函数,出于某种原因,它没有使用参数x,并且声明了一个暂未使用的变量:
int func(int x, int y) {
int i; // 未使用的变量 i
return y; // 未使用函数参数 x
}
GCC在编译代码时会报出警告信息:
In function 'int func(int, int) ':
warning: unused variable 'i' [-Wunused-variable]
At global scope:
warning: unused parameter 'x' [-Wunused-parameter]
int func(int x, int y)
使用 ignore_unused 我们可以显式地忽略这些变量,从而轻易地消除这些警告信息:
int func(int x, int y) {
int i;
ignore_unused(x, i); // 相当于(void)x;(void)i;
return y;
}
显然,ignore_unused比C风格的“(void)var”要更容易理解,无需多余的注释,代码自身说明了一切。
4.2.2 模板用法
ignore_unused库还可以作用于未使用的局部类型定义,它的另一种形式是:
template <typename... Ts>
inline void ignore_unused() {} // 注意没有函数参数列表
ignore_unused的模板用法与函数用法类似,但它不需要函数参数,而是在模板参数列表里写出要忽略的类型。
例如下面的函数内部定义了一个typedef,然后用ignore_unused忽略之:
void func2() {
typedef int result_type; // 暂未使用的类型定义
ignore_unused<result_type>(); // 忽略未使用的类型定义
}
4.3 optional
在实际的软件开发过程中我们经常会遇到“无效值”的情况,例如函数并不是总能返回有效值,很多时候函数正确执行了,但结果却不是合理的值。如果用数学语言来解释,就是返回值位于函数解空间之外。
求一个数的倒数,在实数域内开平方,在字符串中查找子串,它们都可能返回“无效值”。有些无效返回的情况可以用抛出异常的方式来通知用户,但有的情况下这样代价很高或者不允许异常,这时必须要以某种合理的、高效的方式通知用户。
表示“无效值”最常用的做法是增加一个“哨兵”的角色,它位于解空间之外,如NULL、-1、EOF、string::npos、vector::end()等。但这些做法不够通用,而且很多时候不存在解空间之外的“哨兵”。另外一个方法是使用pair
optional使用“容器”语义,包装了“可能产生无效值”的对象,实现了“未初始化”的概念,为这种“无效值”的情形提供了一个更好的解决方案。它已经被收入C++17标准。①
optional 位于名字空间 boost,需要包含头文件
① C++17 标准中的optional接口与boost.optional略有不同,使用std::nullopt_t取代了boost::none_t,并且用 std::in_place_t支持在构造函数里就地创建对象。
optional的类摘要如下:
template<class T>
class optional
{
public :
optional (); // 构造函数
optional ( none_t );
optional ( T const& v );
optional ( bool condition, T v );
optional& operator=( T const& rhs ); // 赋值操作符
template<class... Args> void emplace ( Args...&& args ); // 就地创建
T* operator ->(); // 重载操作符
T& operator *();
T& get(); // 访问值
T* get_ptr();
T& value(); // 访问值,可能抛出异常
T const& value_or( T const& default ) const;
template <typename F> T value_or_eval( F f ) const;
explicit operator bool() const; // 显式 bool 转型
bool operator!() const; // bool 测试
};
optional的真实接口很复杂,因为它要能够包装任何的类型,但实际的接口还是比较简单并且易于理解的,接下来将进行详细说明。
4.3.2 操作函数
optional的模板类型参数T可以是任何类型,就如同一个标准容器对元素的要求,并optional的模板类型参数T可以是任何类型,就如同一个标准容器对元素的要求,不需要T具有默认构造函数,但必须是可拷贝构造的,因为optional需要在内部拷贝值。
可以有很多方式创建optional对象,例如:
- 无参的optional()或者optional(boost::none)构造一个未初始化optional对象;
- optional(v)构造一个已初始化的optional对象,内部拷贝v的值。如果模板类型为T&,那么optional内部持有对引用的包装;
- optional(condition,v)根据条件condition来构造optional对象,如果条件成立(true)则初始化为v,否则为未初始化;
- optional支持拷贝构造和赋值操作,可以从另一个optional对象构造;
- emplace()是一个特殊的“赋值”函数,可以使用参数就地创建对象,避免了构造后再拷贝的代价。
- 想让一个optional对象重新恢复到未初始化状态可以向对象赋none值。
optional采用了指针语义来访问内部保存的元素,这使得optional未初始化时的行为就像一个空指针,可以使用operator bool()和operator!()来检测是否有效。
optional也重载了operator和operator->以实现与指针相同的操作,get()和get_ptr()能够以函数的形式获得元素的引用和指针。注意:它们内部仅使用BOOST_ASSERT(参见6.1节)提供基本的安全保证,如果optional未初始化,那么函数的行为是未定义的。
optional另外提供三个value()系列成员函数,它们比operator和operator->更加安全:
- value()同样可以访问元素,但如果optional未初始化会抛出bad_optional_access异常;
- value_or(default)可以保证返回一个有效的值,如果optional已初始化,那么返回内部的元素,否则返回default;
- value_or_eval(f)类似value_or(),但它的参数是一个可调用的函数或者函数对象,如果optional未初始化则返回f的执行结果即f()。
optional还全面支持比较运算,与普通指针比较的“浅比较”(仅比较指针值)不同,optional的比较是“深比较”,同时加入了对未初始化情况的判断。
4.3.1 类摘要
optional库首先定义了常量boost::none,表示未初始化,明确了“无效值”。
class none_t(); // 定义类型none_t,具体形式依据条件编译而不同
const none_t none = ...; // 定义常量none,具体形式依据条件编译而不同
optional库的核心类是optional,它很像是个仅能存放一个元素的容量,实现了“未初始化”的概念:如果元素未初始化,那么容器就是空的,那么容器就是空的,否则,容器内就是有效的、已经初始化的值。
option的类摘要如下:
template<class T>
class optional
{
public :
optional (); // 构造函数
optional ( none_t );
optional ( T const& v );
optional ( bool condition, T v );
optional& operator= ( T const& rhs ); // 赋值操作符
template<class... Args> void emplace ( Args...&& args ); // 就地创建
T* operator ->(); // 重载操作符
T& operator *();
T& get(); // 访问值
T* get_ptr();
T& value(); // 访问值,可能抛出异常
T const& value_or( T const& default ) const;
template<typename F> T value_or_eval( F f ) const;
explicit operator bool() const; // 显式 bool 转型
bool operator!() const; // bool 测试
};
optional的真实接口很复杂,因为它要能够包装任何的类型,但实际的接口还是比较简单并且易于理解的,接下来将进行详细说明。
4.3.2 操作函数
optional的模板类型参数T可以是任何类型,就如同一个标准容器对元素的要求,并不需要T具有默认构造函数,但必须是可拷贝构造的,因为optional需要在内部拷贝值。
可以有很多方式创建optional对象,例如:
- 无参的optional()或者optional(boost::none)构造一个未初始化optional对象;
- optional(v)构造一个已初始化的optional对象,内部拷贝v的值。
- 如果模板类型为T&,那么optional内部持有对引用的包装;
- optional(condition, v)根据条件condition来构造optional对象,如果条件成立(true)则初始化为v,否则为未初始化;
- optional支持拷贝构造和赋值操作,可以从另一个optional对象构造;
- emplace()是一个特殊的“赋值”函数,可以使用参数就地创建对象,避免了构造后再拷贝的代价。
- 想让一个optional对象重新恢复到未初始化状态可以向对象赋none值。
optional 采用了指针语义来访问内部保存的元素,这使得 optional 未初始化时的行为就像一个空指针,可以使用operator bool()和 operator!()来检测是否有效。
optional也重载了operator和operator->以实现与指针相同的操作,get()和get_ptr()能够以函数的形式获得元素的引用和指针。注意:它们内部仅使用BOOST_ASSERT(参见6.1节)提供基本的安全保证,如果optional未初始化,那么函数的行为是未定义的。
optional另外提供三个value()系列成员函数,它们比operator和operator->更加安全:
4.3.3 用法
optional的接口简单明了,把它认为是一个大小为1并且行为类似指针的容器就可以了,或者把它想象成是一个类似scoped_ptr、shared_ptr的智能指针(但要小心,optional不是智能指针,用法类似但用途不同)。
示范optional基本用法的代码如下:
optional<int> op0; // 一个未初始化的 optional 对象
optional<int> op1(none); // 同上,使用 none 赋予未初始化值
assert(!op0); // bool 测试
assert(op0 == op1); // 比较两个 optional 对象
assert(op1.value_or(253) == 253); // 获取默认值
cout << op1.value_or_eval( // 使用函数对象
[](){return 874;}) << endl; // lambda 表达式定义函数对象
optional<string> ops("test"); // 初始化为字符串 test
cout << *ops << endl; // 用解引用操作符获取值
ops.emplace("monado", 3); // 就地创建一个字符串,没有拷贝代价
assert(*ops == "mon"); // 只使用了前三个字符
这段代码演示了optional的一些基本操作,接下来我们再看一个略微复杂的例子,代码使用optional作为函数的返回值,解决了本节一开始提出的几个问题。
optional<double> calc(int x) { // 计算倒数
return optional<double>(x != 0, 1.0 / x); // 条件构造函数
}
optional<double> sqrt_op(double x) { // 计算实数的平方根
return optional<double>(x >= 0, sqrt(x)); // 条件构造函数
}
optional<double> d = calc(10);
if (d) { // bool 语境测试 optional 的有效性
cout << *d << endl;
}
d = sqrt_op(-10);
if (!d) { // 使用重载的逻辑非操作符
cout << "no result"<< endl;
}
4.3.4 工厂函数
optional提供一个类似make_pair()、make_shared()的工厂函数make_optional(),可以根据参数类型自动推导optional的类型,用来辅助创建optional对象。
它的声明是:
optional<T> make_optional( T const& v );
optional<T> make_optional( bool condition, T const& v );
但make_optional()无法推导出T引用类型的optional对象,如果需要一个optional
make_optional()也不支持emplace的用法,可能存在值的拷贝代价。
示范make_optional()函数用法的代码如下:
auto x = make_optional(5); // 使用 auto 关键字自动推导类型
assert(*x == 5);
auto y = make_optional<double>((*x > 10), 1.0); // 模板参数明确类型
assert(!y);
4.4 assign
许多情况下我们都需要为容器初始化或者赋值,填入大量的数据,比如初始错误代码和错误信息,或者是一些测试用的数据。在C++98中标准容器仅提供了容纳这些数据的方法,但填充的步骤却是相当地麻烦,必须重复调用insert()或者push_back()等成员函数,这正是boost.assign出现的理由。
assign库重载了赋值操作符“+=”、逗号操作符“,”和括号操作符“()”,可以用难以想象的简洁语法非常方便地对标准容器赋值或者初始化。在需要填入大量初值的地方很有用。C++新标准也提供类似功能的初始化列表,但功能没有assign库那么完备。
assign库位于名字空间boost::assign,需要包含头文件
4.4.1 list_inserter
list_inserter是assign库中用来操作容器的工具类,它类似std::back_inserter,但增加了很多操作符重载和助手类来简化代码。
list_inserter的类摘要如下:
template< class Function >
class list_inserter
{
public:
list_inserter& operator,( const T& r ); // 重载 operator,
list_inserter& operator()(); // 重载operator()
list_inserter& operator()( const T& r );
public: // 重复输入数据操作
list_inserter& repeat( std::size_t sz, T r );
list_inserter& repeat_fun( std::size_t sz, Nullary_function fun );
list_inserter& range( SinglePassIterator first, SinglePassIterator last );
list_inserter& range( const SinglePassRange& r );
private:
Function insert_;
};
listinserter内部存储一个函数对象insert用来操作容器,这个函数对象包装了容器的push_back、push_front等操作,例如:
list_inserter& operator,( const T& r ) { // 重载operator,
insert_(r); // 向容器添加元素
return *this; // 返回自身的引用
}
list_inserter成员函数的另一个特点是返回*this,这使得它可以像标准流操作一样串联操作,达到简化代码的目的。
list_inserter还提供repeat/range函数来简化输入重复数据的工作,这几个函数将在4.4.6节介绍。
4.4.2 operator+=
由于list_inserter重载了操作符“+=”和“,”,我们就可以用简洁到令人震惊的语法完成原来用许多代码才能完成的工作,如果不熟悉C++操作符重载的原理你甚至都不会意识到在简洁语法下的复杂工作。
用法
使用assign库时必须使用using指示符,只有这样才能让重载的“+=”“,”等操作符在作用域内生效。例如:
using namespace boost::assign; // 很重要,启用assign 库的功能
vector<int> v; // 标准向量容器
v += 1,2,3,4,5, 6*6; // 用“+=”和“,”填入数据
set<string> s; // 标准集合容器
s += "c", "cpp", "lua", "swift"; // 用“+=”和“,”填入数据
map<int, string> m; // 标准映射容器
m += make_pair(1, "one"),make_pair(2, "two"); // 用“+=”和“,”填入数据
上面的代码示范了assign库操作标准容器的能力。“+=”操作符后可以接若干个可被容器容纳的元素,元素之间使用“,”分隔。元素不一定是常量,表达式或者函数调用也是可以接受的,只要其结果能够转换成容器可容纳的类型。比较特别的是map容器,必须使用辅助函数make_pair()来生成容器元素,单纯地用括号把pair的两个成员括起来是无效的。
实现原理
assign库提供三个工厂函数push_back()、push_front()和insert()。这些函数可作用于拥有同名成员函数的容器,接受容器实例作为参数,创建对应的list_inserter对象。
在名字空间boost::assign里assign库为标准容器重载了operator+=,调用push_back()或insert()函数,形式是:①
inline list_inserter operator+=( C& c, V v ) {
return push_back( c )( v ); // 对于关联容器则是insert函数
}
operator+=作用于容器时调用工厂函数push_back(),产生一个list_inserter对象,以这个对象作为起点,随后的operator()和operator,就会依次执行,使用push_back()或insert()向容器插入数据。由于list_inserter重载了很少使用的逗号操作符,所以函数的调用得到了极大的简化。
operator+=很好用,但有一点遗憾,assign库仅提供了对C++98标准容器(vector、list、set等)的重载,要操作其他类型的容器(如Boost容器)只能依据其原理自行实现。
① 但 assign 未及时跟进C++标准,没有为准单向链表forward_list 提供operator+=。
4.4.3 operator()
operator+=仅作用于标准容器,而且在处理map容器时也显得有些麻烦,所以我们可以直接使用工厂函数insert()/push_front()/push_back(),利用它们返回的list_inserter对象来填入数据。示范工厂函数insert()、push_front()、push_back()用法的代码如下:
using namespace boost::assign; // 很重要,启用assign 库的功能
vector<int> v;
push_back(v)(1)(2)(3)(4)(5); // 使用push_back 工厂函数
list<string> l;
push_front(l)("c")("cpp")("lua")("swift"); // 使用push_front 工厂函数
forward_list<string> fl; // C++的单向链表容器
push_front(l)("matrix")("reload"); // 使用push_front工厂函数
set<double> s;
insert(s)(3.14)(0.618)(1.732); // 使用insert工厂函数
map<int, string> m;
insert(m)(1, "one")(2, "two"); // 使用insert工厂函数
这段代码与使用operator+=没有太多的不同。对于拥有push_back()或push_front()成员函数的容器(vector、list、deque),可以调用assign::push_back()或assign::push_front(),而对于set和map,则只能使用assign::insert()。
operator()的好处是可以在括号中使用多个参数,这对于map这样的元素是由多个值组成的类型非常方便,避免了make_pair()函数的使用。如果括号中没有参数,那么list_inserter将调用容器元素的默认构造函数填入一个默认值,而逗号操作符则不能这样做。
括号操作符也可以与逗号等操作符混合使用,写法更简单,有时甚至看起来不像是合法的C++代码(但的确是完全正确的C++代码),例如:
vector v;
push_back(v),1,2,3,4,5;
push_back(v)(6),7,64/8,(9),10; // v = [ 1 2 3 4 5 6 7 8 9 10 ]
deque d;
push_front(d)() = "breath","of","the","wild";
assert(d.size()==5); // d = [' wild', 'the', 'of', 'breath', '']
4.4.4 generic_list
list_inserter解决了对容器的赋值问题,但有的时候我们需要在容器构造的时候就完成数据的填充,这种方式较赋值更为高效。
C++11标准引入了初始化列表std::initializer_list,而boost.assign库则提供功能类似的generic_list,它的类摘要如下:
template< class T >
class generic_list
{
public:
iterator begin() const; // 类似容器的接口
iterator end() const;
bool empty() const;
size_type size() const;
generic_list& operator,( const Ty& u ); // 重载operator,
generic_list& operator()(); // 重载operator()
generic_list& operator()( const Ty& u );
public: // 重复输入数据操作
generic_list& repeat( std::size_t sz, U u );
generic_list& repeat_fun( std::size_t sz, Nullary_function fun );
generic_list& range( SinglePassIterator first, SinglePassIterator last );
generic_list& range( const SinglePassRange& r );
public: // 容器转换
operator Container() const;
Container to_container( Container& c ) const;
adapter_converter to_adapter() const;
Adapter to_adapter( Adapter& a ) const;
Array to_array( Array& a ) const;
};
与list_inserter类似,generic_list也重载了逗号和括号操作符,因为要在初始化时与容器互操作,它还增加了一些容器操作函数。
generic_list内部使用std::deque存储元素,大多数操作都转换为deque的push_back(),例如:
generic_list& operator,( const Ty& u ) { // 重载operator,
this->push_back( u ); // push_back 添加元素
return *this; // 返回自身的引用
}
4.4.5 初始化容器
assign库提供三个工厂函数list_of()、map_list_of()/pair_list_of()和tuple_list_of(),它们能够产生generic_list对象,然后我们就可以像list_inserter一样使用operator()和operator,来填充数据。
因为generic_list提供到容器类型的隐式转型操作,所以它可以赋值给任意容器,当然我们也可以显式调用容器转换函数。
list_of
ist_of()函数的声明是:
inline generic_list<T> list_of();
inline generic_list<T> list_of( const T& t );
它的用法与之前的insert()、push_back()等函数很相似:
vector<int> v = list_of(1)(2)(3)(4)(5); // v = [1, 2, 3, 4, 5]
deque<string> d = (list_of("power")("bomb"),"phazon","suit"); // 注意括号和逗号的使用
// d = [power bomb phazon suit]
set<int> s = (list_of(10), 20,30,40,50); // 注意括号和逗号的使用
// s = {10 20 30 40 50}
map<int, string> m = list_of(make_pair(1, "one"))(make_pair(2, "two")); // m = [(1, “one”) (2, “two”)]
list_of()函数可以全部使用括号操作符,也可以把括号与逗号结合起来,但使用后者时需要将整个list_of表达式用括号括起来,否则编译器会无法推导出list_of的类型导致赋值失败。
上面的代码对应C++初始化列表的代码是:
vector<int> v = {1,2,3,4,5};
deque<string> d = {"power","bomb","phazon","suit"};
set<int> s = {10,20,30,40,50};
map<int, string> m = {{1, "one"},{2, "two"}};
两段代码比较起来,虽然assign库的略显麻烦,但胜在对C++各版本的兼容性。
map_list_of/pair_list_of
使用list_of()处理map容器不是很方便,于是map_list_of()/pair_list_of()应运而生,map_list_of()可以接受两个参数,然后自动构造std::pair对象插入map容器,pair_list_of()则纯粹是map_list_of的同义词,两者的用法功能完全相同。
map_list_of()和pair_list_of()的基本形式如下:
template< class Key, class T >
map_list_of( const Key& k, const T& t ); // key,value
template< class F, class S >
pair_list_of( const F& f, const S& s ) { // first,second
return map_list_of( f, s );
}
下面的代码演示了map_list_of的用法:
map<int, int> m1 = map_list_of(1, 2)(3, 4)(5, 6); // m1 = [(1, 2)(3, 4)(5, 6)]
map<int, string> m2 = map_list_of(1, "one")(2, "two"); // m2 = [(1, "one")(2, "two")]
tuple_list_of
tuple_list_of用于初始化元素类型为tuple的容器,tuple是Boost引入的一种新的容器/数据结构,关于tuple和的tuple_list_of的用法请参见7.6.8节。
4.4.6 重复输入
在填充数据时会遇到输入重复数据的问题,如果用之前的方法要写大量的重复代码,很麻烦,也容易造成多写或少写的错误。list_inserter和generic_list都提供成员函数repeat()、repeat_fun()和range()来减轻工作量。
这三个函数的简要声明如下:
list& repeat( std::size_t sz, U u );
list& repeat_fun( std::size_t sz, Nullary_function fun );
list& range( SinglePassIterator first, SinglePassIterator last );
list& range( const SinglePassRange& r )
repeat()函数把第二个参数作为要填入的值,重复第一个参数指定的次数,与vector、deque等容器的构造函数很相似;repeat_fun()函数同样重复第一个参数的次数,但第二个参数是个无参的函数或函数对象,它返回填入的数值;range()函数则可以把一个序列里的全部或者部分元素插入到另一个序列里。
它们同样也返回列表list_inserter 或generic_list,所以可以串联进operator()和operator,操作序列里。
示范它们用法的代码如下:
vector<int> v = list_of(1).repeat(3, 2)(3)(4)(5); // v = 1,2,2,2,3,4,5
multiset<int> ms;
insert(ms).repeat_fun(5, &rand).repeat(2, 1), 10; // ms = x,x,x,x,x,1,1,10
deque<int> d;
push_front(d).range(v.begin(), v.begin() + 5); // d = 3,2,2,2,1
4.4.7 操作非标准容器
assign库不仅支持标准容器(vector、string、deque、list、set、multiset、map、multimap),也对容器适配器提供了适当的支持,包括stack、queue和priority_queue。
因为这三个容器适配器没有push_back/push_front函数,所以assign库提供push()函数来赋值,但stack可以使用operator+=。初始化的list_of表达式最后则使用to_adapter()成员函数来适配到非标准容器。如果使用逗号操作符还需要把整个表达式用括号括起来,才能使用点号操作符调用to_adapter()。
示范assign库应用于容器适配器的代码如下:
stack<int> stk = (list_of(1), 2, 3).to_adapter();
stk += 4, 5, 6;
for (; !stk.empty();) { // 输出stack 的内容
cout << stk.top() << " ";
stk.pop();
}
queue<string> q = (list_of("china")("us")("uk")).repeat(2, "russia").to_adapter();
push(q)("germany");
for (; !q.empty();) { // 输出queue 的内容
cout << q.front() << " ";
q.pop();
}
priority_queue<double> pq = (list_of(1.414), 1.732, 2.236).to_adapter();
push(pq), 3.414, 2.71828;
for (; !pq.empty();) { // 输出优先队列的内容
cout << pq.top() << " ";
pq.pop();
}
/*
6 5 4 3 2 1
china us uk russia russia germany
3.414 2.71828 2.236 1.732 1.414
*/
assign库也支持大部分Boost库容器,如array、circular_buffer、unordered等—只要它们有push_back/insert等成员函数,用法与标准容器基本类似,在第7章可以找到更多的示例代码。
4.4.8 其他议题
assign库还有两个类似功能的函数ref_list_of()和cref_list_of(),这两个函数接受变量的引用作为参数来创建初始化列表,较list_of()的内部deque效率更高,但用法略微麻烦,例如:
int a = 1, b = 2, c = 3;
vector<int> v = ref_list_of<3>(a)(b)(c);
assert(v.size() == 3);
assign库还特别支持Boost中的指针容器,提供ptr_push_back()、ptr_list_of()等函数。由于本书未涉及指针容器,故在这里不做介绍,感兴趣的读者请阅读推荐书目[3]。
4.5 tribool
boost.tribool类似C++内建的bool类型,但基于三态的布尔逻辑:在true(真)和false(假)之外还有一个indeterminate状态(未知、不确定)。三态布尔逻辑的一个例子场景是执行某项任务,在执行之前状态是未知的(没有开始也没有结束),启动任务后状态是true,停止任务后状态是false。
tribool位于名字空间boost::logic,但为了方便使用被using语句引入了boost名字空间,需要包含头文件
4.5.1 类摘要
tribool的类摘要如下:
class tribool
{
public:
tribool(bool value); // 默认构造函数
tribool(bool initial_value); // bool 值构造函数
tribool(indeterminate_keyword_t); // 初始化为不确定值
operator safe_bool() const; // bool 转型
enum value_t { false_value, true_value, indeterminate_value } value;
};
bool indeterminate(tribool x); // 判断是否是不确定状态
tribool operator!(tribool x); // 逻辑非运算
... // 其他逻辑运算符和比较运算符重载
tribool类很简单,它内部实现了三态bool值的表示,除了构造函数没有什么其他成员函数。可以在创建tribool对象的同时传入三态bool值对它进行初始化,如果使用无参的默认构造函数,那么tribool默认值是false。对tribool的操作都是通过逻辑运算符和比较运算符的重载来完成的,支持的逻辑运算包括||、&&和!,比较运算支持==和!=,这些操作都可以任意混合bool和tribool一起运算。不确定状态indeterminate是一个特殊的tribool值,它与bool值true、false的运算遵循三态布尔逻辑:
- 任何与indeterminate的比较操作结果都是indeterminate;
- 与indeterminate的逻辑或运算(||)只有与true运算结果为true,其他均为indeterminate;
- 与indeterminate的逻辑与运算(&&)只有与false运算结果为false,其他均为indeterminate;
- indeterminate的逻辑非操作(!)结果仍为indeterminate。
自由函数indeterminate()可以判断一个tribool是否处于不确定状态。
4.5.2 用法
tribool可以像普通的bool类型一样使用,只是多出了一个indeterminate(未知、不确定)的取值。如果仅使用true和false两个值,那么tribool与bool的用法是完全相同的,但如果需要使用indeterminate值,就必须遵循三态布尔的逻辑运算规则。
示范tribool基本用法的代码如下:
tribool tb(true); // 值为true 的tribool
tribool tb2(!tb); // 值为false
if (tb) { // tb==true
cout << "true" << endl;
}
tb2 = indeterminate; // tb2 是不确定状态
assert(indeterminate(tb2)); // 用indeterminate 函数检测状态
cout << tb2 << endl;
if (tb2 == indeterminate) { // 与indeterminate 值比较,无意义
cout << "indeterminate" << endl;
} // 这行语句不会执行
if (indeterminate(tb2)) { // 用indeterminate 函数检测状态
cout << "indeterminate" << endl;
} // 输出"indeterminate"
cout << (tb2 || true) << endl; // 逻辑或运算,输出true
cout << (tb2 && false) << endl; // 逻辑与运算,输出false
在处理tribool的不确定状态时必须要小心,因为它既不是true也不是false,使用它进行条件判断永远都不会成立,判断不确定状态必须要使用indeterminate()函数:
tribool tb(indeterminate);
if (tb) // 永远不成立
cout << "never reach here" << endl;
if (!tb) // 永远不成立
cout << "never reach here" << endl;
if (indeterminate(tb)) // 必须使用indeterminate
cout << "indeterminate" << endl;
4.5.3 为第三态更名
tribool的第三态类型indeterminate_keyword_t实际上是一个函数指针类型:
typedef bool (*indeterminate_keyword_t)(tribool, detail::indeterminate_t);
而indeterminate实际上只是一个符合indeterminate_keyword_t类型的函数,它的真正功能是用来判断tribool对象内部值是否是第三态,但在tribool的构造函数里被当做一个标志,只要是indeterminate_keyword_t类型那么就置为不确定状态:
tribool(indeterminate_keyword_t) : // 构造函数,接受特殊的参数
value(indeterminate_value) {} // 置为不确定状态
作为第三态的名字indeterminate很清晰明确但可能有些长,所以tribool允许把indeterminate改变成任意用户喜欢的名字,常用的名字可以是unknown、maybe等。
只需要在全局域内使用宏BOOST_TRIBOOL_THIRD_STATE就可以为第三态更名,像这样:
BOOST_TRIBOOL_THIRD_STATE(unknown)
然后我们就可以随意使用这个新名字代替原来的indeterminate:
tribool tb(unknown); // 可以作为不确定值
assert(unknown(tb)); // 可以作为检测函数
assert(unknown(tb || false));
宏只是定义了一个新的indeterminate_keyword_t类型的函数而已,类似于:
inline bool some_name(tribool x){
return x.value == tribool::indeterminate_value;
}
因为宏BOOST_TRIBOOL_THIRD_STATE 实质上定义了一个函数,而C++不允许函数嵌套,所以这个宏最好在全局域使用,它将在定义后的整个源代码中都生效。
如果把 BOOST_TRIBOOL_THIRD_STATE 用在一个名字空间里,那么新的第三态名字将成为名字空间的一个成员,使用时需加上名字空间限定,例如:
namespace tmp_ns { // 一个临时名字空间
BOOST_TRIBOOL_THIRD_STATE(unknown)
};
tribool tb(tmp_ns::unknown); // 使用名字空间限定
4.5.4 输入/输出
tribool可以如bool类型一样进行流操作,但需要包含另外一个头文件
如果设置了流的boolalpha标志,则对应字符串”false”、”true”和”indeterminate”。
例如:
tribool tb1(true), tb2(false), tb3(indeterminate);
cout << tb1 << "," // 输出1
<< tb2 << "," // 输出0
<< tb3 << endl; // 输出2
4.5.5 其他议题
optional
optional<bool> b;
if (!b) { // b未初始化,既不是true 也不是false
cout << "indeterminate" << endl;
}
b = false;
if (b) { // b有值false
cout << "b=" << *b << endl;
}
但optional
optional
由于optional支持bool转换,用于检测optional是否已经初始化,所以在bool语境下如果不注意optional的这个特性很容易导致意外的错误。例如,下面的代码本意是想使用optional内的bool值作为if语句的判断条件,但实际上条件判断的是optional未初始化:
optional<bool> b(false);
if (!b) { // optional 的bool 转换
cout << "false" << endl;
}
// 正确的写法应该是
if(b && !*b){ // b已经初始化且值为false
cout << "false" << endl;
}
选择optional
4.6 operators
C++提供了强大且自由的操作符重载能力,可以重新定义大多数操作符的行为,使操作更加简单直观。这方面很好的例子就是标准库中的string和complex,可以像操作内置类型int、double那样对它们进行算术运算和比较运算,非常方便。
但实现重载操作符却比使用它要麻烦许多,因为很多运算具有对称性,如果定义了operator+,那么很自然需要operator-,如果有小于比较,那么也应该有小于等于、大于、大于等于比较。完全实现这些操作符的重载工作是单调乏味的,而且增加的代码量也增加了出错的可能性,还必须保证这些操作符都实现了正确的语义。
实际上很多操作符可以从其他的操作符自动推导出来,例如a!=b可以是!(a==b),a>=b可以是!(a、<=、>=,只需要为类定义了==和<操作符,那么这四个操作符就可以自动实现。
#include <utility>
class demo_class // 一个定义operator<的类
{
public:
demo_class(int n): x(n) {
}
int x;
friend bool operator<(const demo_class& l, const demo_class& r) {
return l.x < r.x;
}
};
int main() {
demo_class a(10), b(20);
using namespace std::rel_ops; // 打开std::rel_ops 名字空间
assert(a < b); // 自定义的<操作符
assert(b >= a); // >=等操作符被自动实现
}
但std::rel_ops的解决方案过于简单,还很不够。除了比较操作符,还有很多其他的操作符重载标准库没有给出解决方案,而且使用这些操作符需要用using语句打开std::rel_ops名字空间,不方便,也会带来潜在的冲突风险。
boost.operators库因此应运而生。它采用类似std::rel_ops的实现手法,允许用户在自己的类里仅定义少量的操作符(如“<”),就可方便地自动生成其他操作符重载,而且保证正确的语义实现。
operators位于名字空间boost,需要包含头文件
4.6.1 基本运算概念
由于C++可重载的操作符非常多,因此operators库是由多个类组成的,分别用来实现不同的运算概念,比如less_than_comparable定义了“<”系列操作符,left_shiftable定义了“<<”系列操作符。
operators中的概念很多,囊括了C++中的大部分操作符重载,在这里我们先介绍一些最常用的算术操作符:
- equality_comparable : 要求提供==,可自动实现!=,相等语义;
- less_than_comparable : 要求提供<,可自动实现>、<=、>=;
- addable : 要求提供+=,可自动实现+;
- subtractable : 要求提供-=,可自动实现-;
- incrementable : 要求提供前置++,可自动实现后置++;
- decrementable : 要求提供前置—,可自动实现后置—。
- equivalent : 要求提供<,可自动实现==,等价语义,它与equality_comparable 的区别请参见4.6.5 节。
这些概念在库中以同名类的形式提供,用户需要以继承的方式来使用它们。继承的修饰符并不重要(private、public都可以),因为operators库里的类都是空类,没有成员变量和成员函数,仅定义了数个友元操作符函数。例如,less_than_comparable的形式是:①
template <class T>
struct less_than_comparable {
friend bool operator>(const T& x, const T& y);
friend bool operator<=(const T& x, const T& y);
friend bool operator>=(const T& x, const T& y);
};
① 这段代码不是Boost 程序的真实代码,实际上less_than_comparable 是less_than_comparable1的子类。
4.6.2 算术操作符
我们使用一个三维空间的点point 作为operators 库的示范类:
class point {
int x, y, z;
public:
explicit point(int a = 0, int b = 0, int c = 0) : x(a) , y(b) , z(c) {
}
void print() const {
cout << x << "," << y << "," << z << endl;
}
};
我们先来实现less_than_comparable,它要求point类提供“<”操作符,并由它继承。假定point的小于关系是由三个坐标值的平方和决定的,下面的代码示范了less_than_comparable的用法,只需要为point增加父类,并定义less_than_comparable概念所要求的operator<:
class point : boost::less_than_comparable<point> // 小于关系,私有继承
{
public:
friend bool operator<(const point& l, const point& r)
{
return (l.x * l.x + l.y * l.y + l.z * l.z < r.x * r.x + r.y * r.y + r.z * r.z);
}
... // 其他成员函数
};
less_than_comparable作为基类的用法可能稍微有点奇怪,它把子类point作为了父类的模板参数:less_than_comparable
//template<T = point>
struct less_than_comparable {
friend bool operator>=(const point& x, const point& y) {
return !(x < y);
}
}
明白了less_than_comparable的继承用法,剩下的就很简单了:point类定义了一个友元operator<操作符,然后其余的>、<=、>=就由less_than_comparable自动生成。几乎不费什么力气,在没有“污染”名字空间的情况下我们就获得了四个操作符的能力:
point p0, p1(1, 2, 3), p2(3, 0, 5), p3(3, 2, 1);
assert(p0 < p1 && p1 < p2);
assert(p2 > p0);
assert(p1 <= p3);
assert(!(p1 < p3) && !(p1 > p3));
同样我们可以定义相等关系,使用equality_comparable,规则是point 的三个坐标值完全相等,需要自行实现operator==:
class point : boost::less_than_comparable<point>, // 使用多重继承
boost::equality_comparable<point> // 新增相等关系
{
public:
friend bool operator<(const point& l, const point& r) { /*同前*/
}
friend bool operator==(const point& l, const point& r) {
return r.x == l.x && r.y == l.y && r.z == l.z;
}
};
然后我们就自动获得了operator!=的定义:
point p0, p1(1,2,3), p2(p1), p3(3,2,1);
assert(p1 == p2);
assert(p1 != p3);
在使用operators库时要注意一点,模板类型参数必须是子类自身,特别是当子类本身也是个模板类的时候,不要错写成子类的模板参数或者子类不带模板参数的名称,否则会造成编译错误。假如我们改写point类为一个模板类:
template<typename T> class point {...};
那么如下的形式都是错误的:
template<typename T> class point:boost::less_than_comparable<T>
template<typename T> class point:boost::less_than_comparable<point>
正确的写法应该是
template<typename T> class point:boost::less_than_comparable<point<T>>
4.6.3 基类链
多重继承一直是C++中引发争论的话题,喜欢它的人和讨厌它的人几乎同样多。总的来说,多重继承是一种强大的面向对象技术,但使用不当也很容易引发诸多问题,比如难以优化和经典的“钻石型”继承。
operators库使用泛型编程的“基类链”技术解决了多重继承的问题,这种技术通过模板把多继承转换为链式的单继承。
前面当讨论到less_than_comparable
operators库的操作符模板类除了接受子类作为比较类型外,还可以接受另外一个类,作为它的父类,由此可以无限串联在一起(但要受编译器的模板编译能力限制),像这样:
demo: x< demo, y<demo, z<demo,...> > >
使用基类链技术,point类的基类部分可以是这样:
boost::less_than_comparable<point, // 注意这里
boost::equality_comparable<point> > // 是一个有很大模板参数列表的类
对比一下多重继承的写法:
boost::less_than_comparable<point>, // 注意这里
boost::equality_comparable<point> // 有两个类
代码非常相似,区别仅仅在于模板参数列表结束符号“>”的位置,如果不仔细看可能根本察觉不出差距。但正是这个小小的差距,使基类链通过模板组成了一连串的单继承链表,而不是多个父类的多重继承。<br /> 例如,如果为point类再增加加法和减法定义,则继承列表就是
class point:
less_than_comparable<point, // 小于操作
equality_comparable<point, // 相等操作
addable<point, // 加法操作
subtractable<point // 减法操作
>
>
>
> {...};
基类链技术会导致代码出现一个有趣的形式:在派生类的基类声明末尾处出现一长串的“>”(模板声明的结束符)。编写代码时需要小心谨慎以保证尖括号的匹配,使用良好的代码缩进和换行可以减少错误的发生。
4.6.4复合运算概念
基类链技术解决了多重继承的效率问题,但它也带来了新的问题,为了使用操作符概念需要写出很长的基类链代码。因此operators库使用基类链把一些简单的运算概念组合成了复杂的概念,即复合运算。这是个很自然的要求,如果有<,则当然会需要==,如果有了+,则可能还需要-、*和/。复合运算不仅进一步简化了代码的编写,给出了更明确的语义,它也可以避免用户代码中基类链过长的问题。
operators库提供的常用复合运算概念如下:
4.7 exception
异常是C++错误处理的重要机制,它改变了传统的使用错误返回值的处理模式,简化了函数的接口和调用代码,有助于编写整洁、优雅、健壮的程序。C++标准定义了标准异常类std::exception及一系列子类,是整个C++语言错误处理的基础。<br /> boost.exception库针对标准库中异常类的缺陷进行了强化,提供<<操作符重载,可以向异常传入任意数据,有助于增加异常的信息和表达力,其中的部分特性已经被收入C++标准。<br /> exception位于名字空间boost,需要包含头文件<boost/exception/all.hpp>。
4.7.1 标准库中的异常
在使用boost.exception之前,我们需要先了解C++标准规定的异常体系。<br /> C++标准中定义了一个异常基类std::exception和try/catch/throw异常处理机制,std::exception又派生出若干子类,用以描述不同种类的异常,如bad_alloc、bad_cast、out_of_range等,共同构建了C++异常处理框架。<br /> C++允许任何类型作为异常抛出,但在std::exception出现后,我们应该尽量使用它,因为std::exception提供了一个很有用的成员函数what(),可以返回异常所携带的信息,这比简单地抛出一个整数错误值或者字符串更好更安全。<br /> 如果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;
}
};
但当系统中需要很多不同种类的异常时,这种实现方法很快就成为了程序员的沉重负担—为了容纳不同的信息需要编写大量极其相似的代码。<br /> 而且这种解法还存在一个问题:很多时候当发生异常时不能获得有关异常的完全诊断信息,而标准库的异常类一旦被抛出,它就成为了一个“死”对象,程序失去了对它的控制能力,只能使用它或者再抛出一个新的异常。<br /> boost.exception针对这些缺陷定义了新的异常类boost::exception,完善了C++标准的异常处理机制。<br />接下来的讨论在说到exception时,如果不特别指明,均是指boost::exception,标准库的异常使用std::exception的形式。
4.7.2类摘要
exception库提供两个类:exception和error_info,它们是exception库的基础。
exception
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不会被误用。<br /> exception的重要能力在于其友元操作符<<,可以存储error_info对象的信息,存入的信息可以用自由函数get_error_info()随时再取出来。这个函数返回一个存储数据的指针,如果exception里没有这种类型的信息则返回空指针。<br /> exception特意没有从std::exception继承,因为现实中已存的大量代码已经有很多std::exception的子类,而如果exception也是std::exception的子类,则对exception再使用继承可能会引发“钻石型”多重继承问题。①
①应该总对异常类使用虚继承,这是由异常的特殊处理机制决定的。但现实通常不那么理想,从现在开始,对boost::exception应该总使用虚继承。
error_info
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是一个标记,我们已经在3.11节见过了类似的用法。它通常(最好)是一个空类,仅用来标记error_info类,使它在模板实例化时生成不同的类型。第二个模板类型参数T是真正存储的信息数据,可以用成员函数value()访问。
4.7.3 向异常传递信息
exception和error_info被设计为配合std::exception一起工作,自定义的异常类可以安全地从exception和std::exception多重继承,从而获得两者的能力。<br /> 因为exception被定义为抽象类,因此我们的程序必须定义它的子类才能使用它,如前所述,exception必须使用虚继承的方式。通常,继承完成后自定义异常类的实现也就结束了,不需要“画蛇添足”地向它增加成员变量或者成员函数,这些工作都已经由exception完成了。例如:
struct my_exception : // 自定义异常类
virtual std::exception, // 虚继承,struct默认public继承
virtual boost::exception // 虚继承,struct默认public继承
{ // 空实现,不需要实现代码
};
接下来我们需要定义异常需要存储的信息—使用模板类error_info。用一个struct作为第一个模板参数来标记信息类型,再用第二个模板参数指定信息的数据类型。由于error_info的类型定义较长,为了使用方便起见,通常需要使用typedef。<br /> 下面的代码使用error_info定义了两个存储int和string的信息类:
// 异常信息的类型
typedef boost::error_info<struct tag_err_no, int> err_no;
typedef boost::error_info<struct tag_err_str, string> err_str;
示范exception用法的代码如下:
#include <boost/exception/all.hpp>
#include <iostream>
using namespace std;
using namespace boost;
struct my_exception : // 自定义异常类
virtual std::exception, // 虚继承,struct默认public继承
virtual boost::exception // 虚继承,struct默认public继承
{ // 空实现,不需要实现代码
};
int main(int argc, char* argv[])
{
typedef boost::error_info<struct tag_err_no, int> err_no;
typedef boost::error_info<struct tag_err_str, string> err_str;
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; // 获得异常信息
}
}
/*
10
Unknown exception
other info
*/
代码里的注释已经解释了很多东西,我们再来看一下。<br />程序首先定义了一个异常类my_exception,然后使用typedef定义了两种异常信息:err_no和err_str,用int和string分别存储错误码和错误信息。main()函数使用function-try块来捕获异常,它把整个函数体都包含在try块中,可以更好地把异常处理代码与正常流程代码分离。<br />throwmy_exception()语句创建了一个my_exception异常类的临时对象,并立刻使用<<向它传递了err_no对象,存入错误码10。随后,异常被catch块捕获,自由函数get_error_info<err_no>(e)可以获得异常内部保存的信息值的指针,所以需要用解引用操作符*访问。<br />异常还可以被追加信息,同样使用操作符<<,最后在function-try的catch块部分,异常被最终处理,程序结束。
4.7.4 错误信息类
通过例子我们基本了解了exception的用法,可以看到exception的使用是相当清晰简单的,同时又提供了灵活强大的功能,使异常类有了更多的用途。<br />由于从exception派生的异常类定义非常简单,没有实现代码,所以可以很容易地建立起一个适合自己程序的、精细完整的异常类体系,使每个异常类只对应一种错误类型。只要都使用虚继承,类体系可以任意复杂,充分表达错误的含义。<br />处理异常的另一个重要工作是定义错误信息类型,基本方法是使用typedef来具体化error_info模板类。这通常比较麻烦,特别是有大量信息类型的时候。因此exception库特意提供若干预先定义好的错误信息类,如同标准库定义的logic_err等类型,使程序员用起来更轻松:
typedef error_info<...> errinfo_api_function;
typedef error_info<...> errinfo_at_line;
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(参见4.8.2节)、__FILE__和__LINE__使用,可以获得调用函数名、源文件名和源代码行号。<br /> 但如果这些预定义类不能满足要求,我们还要再使用typedef。为解决这个不大不小的麻烦,我们可以自定义一个辅助工具宏DEFINE_ERROR_INFO,它可以方便快捷地实现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;
4.7.5 包装标准异常
exception库提供一个模板函数enable_error_info(T&e),其中T是标准异常类或者其他自定义类型。它可以包装类型T,产生一个从boost::exception和T派生的类,从而在不修改原异常处理体系的前提下获得boost::exception的所有好处。如果类型T已经是boost::exception的子类,那么enable_error_info将返回e的一个拷贝。<br />enable_error_info()通常用在程序中已经存在异常类的场合,对这些异常类的修改很困难甚至不可能(比如已经编译成库)。这时候enable_error_info()就可以包装原有的异常类,从而很容易地在不变动任何已有代码的基础上把boost::exception集成到原有异常体系中。<br />示范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来捕获异常。<br /> enable_error_info()也可以包装标准库的异常:
throw enable_error_info(std::runtime_error("runtime"))
<< errinfo_at_line(__LINE__); // 包装标准异常
4.7.6 使用函数抛出异常
exception库为异常增加了许多新的功能,带来了很多好处,但由于各种各样的原因,程序中的异常类并不能总是从boost::exception继承,必须使用enable_error_info()来包装。<br /> 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自动向异常添加了发生异常的函数名、文件名和行号等信息。<br /> 但需要注意一点,为了保证与配置宏BOOST_NO_EXCEPTIONS兼容(这个宏本书不做介绍),throw_exception()函数和BOOST_THROW_EXCEPTION宏都要求参数e必须是std::exception的子类。<br /> 对于新写的异常类来说这通常不是问题,因为它们总是从std::exception和boost::exception继承,但假如旧代码中有非std::exception的派生异常就无法使用,这在一定程度上限制了throw_exception()和BOOST_NO_EXCEPTIONS的应用范围。<br /> 如果确保在程序中总使用boost::exception,不会去定义配置宏BOOST_NO_EXCEPTIONS(这应该是大多数情况),那么可以修改Boost源代码,在<boost/throw_exception.hpp>里注释掉throw_exception_assert_compatibility(e)这条语句,以取消这个限制。
