头文件 <boost/operators.hpp>
在 namespace boost
中提供了几组类模板。这些模板根据类提供的少量基本运算符来定义命名空间范围内的运算符。
类类型的重载运算符通常成组出现。如果您能编写 x + y
,您可能也希望能够编写 x += y
。如果您能编写 x < y,
您也希望有 x > y
,x >= y,
和 x <= y
。
此外,除非您的类具有非常令人惊讶的行为,否则其中一些相关的运算符可以用其他运算符来定义(例如,x >= y
等效于 !(x < y)
)。
对多个类复制此样板代码既乏味又容易出错。 <boost/operators.hpp>
模板通过基于您在类中定义的其他运算符为您生成命名空间范围内的运算符来提供帮助。
例如,如果您声明一个这样的类
class MyInt
: boost::operators
<MyInt>
{
bool operator<(const MyInt& x) const;
bool operator==(const MyInt& x) const;
MyInt& operator+=(const MyInt& x);
MyInt& operator-=(const MyInt& x);
MyInt& operator*=(const MyInt& x);
MyInt& operator/=(const MyInt& x);
MyInt& operator%=(const MyInt& x);
MyInt& operator|=(const MyInt& x);
MyInt& operator&=(const MyInt& x);
MyInt& operator^=(const MyInt& x);
MyInt& operator++();
MyInt& operator--();
};
那么 operators
<> 模板会添加十几个额外的运算符,例如 operator>
,operator<=
,operator>=
和二元 operator+
。
还提供了模板的二元形式,以允许与其他类型交互。
关于概念用法的说明:所讨论的概念不一定是标准库的概念,例如可复制构造,尽管其中一些可能是;它们是我们所说的小写'c'的概念。特别是,它们与前者不同之处在于它们不描述它们要求定义的运算符的精确语义,除了 (a) 将一个概念中分组的运算符的语义应保持一致(例如,计算 a += b
和 a = a + b
表达式的效果应该相同),以及 (b) 运算符的返回类型应遵循内置类型的相应运算符的返回类型的语义(例如,operator<
应返回可转换为 bool
的类型,并且 T::operator-=
应返回可转换为 T
的类型)。这种“宽松”的要求使 operators
库适用于来自不同领域的更广泛的目标类集,即最终更有用。
此示例演示如何将一些算术运算符模板与几何点类模板一起使用。
template <class T> class point // note: private inheritance is OK here! : boost::addable< point<T> // point + point , boost::subtractable< point<T> // point - point , boost::dividable2< point<T>, T // point / T , boost::multipliable2< point<T>, T // point * T, T * point > > > > { public: point(T, T); T x() const; T y() const; point operator+=(const point&); // point operator+(point, const point&) automatically // generated by addable. point operator-=(const point&); // point operator-(point, const point&) automatically // generated by subtractable. point operator*=(T); // point operator*(point, const T&) and // point operator*(const T&, point) auto-generated // by multipliable. point operator/=(T); // point operator/(point, const T&) auto-generated // by dividable. private: T x_; T y_; }; // now use the point<> class: template <class T> T length(const point<T> p) { return sqrt(p.x()*p.x() + p.y()*p.y()); } const point<float> right(0, 1); const point<float> up(1, 0); const point<float> pi_over_4 = up + right; const point<float> pi_over_4_normalized = pi_over_4 / length(pi_over_4);
二元运算符的参数通常具有相同的类型,但定义组合不同类型的运算符的情况并不少见。例如,可能需要将数学向量乘以标量。为此目的提供了算术运算符模板的二元模板形式。应用模板的二元形式时,运算符的所需返回类型通常决定了应该从运算符模板派生的两个类型中的哪个类型。
例如,如果 T + U
的结果是 T
类型,则应从 addable<T, U>
派生 T
(而不是 U
)。比较模板 less_than_comparable<T,U>
、equality_comparable<T, U>
、equivalent<T, U>
和 partially_ordered<T, U>
是此准则的例外,因为它们定义的运算符的返回类型是 bool
。
在不支持部分特化的编译器上,必须使用下面显示的名称以及尾随 '2'
来指定二元形式。为了对称性和为了启用基类链接技术的某些应用,提供了尾随 '1'
的单参数形式。
混合算术运算:二元模板形式的另一个应用是针对类型 T
和可转换为 T
的类型 U
的混合算术运算。在这种情况下,二元模板形式有两个有用的方面:一是提供运算符重载的相应签名,二是性能。
关于运算符重载,假设例如 U
是 int
,T
是用户定义的无限制整数类型,并且存在 double operator-(double, const T&)
。
如果要计算int - T
,并且没有提供T operator-(int, const T&)
,编译器会认为double operator-(double, const T&)
比T operator-(const T&, const T&)
更匹配,这可能与用户的意图不同。
为了定义一组完整的运算符签名,提供了两个参数模板形式的附加“左”形式subtractable2_left<T, U>
、dividable2_left<T, U>
和modable2_left<T, U>
,这些形式定义了非交换运算符的签名,其中U
出现在左侧(operator-(const U&, const T&)
,operator/(const U&, const T&)
,operator%(const U&, const T&)
)。
关于性能,请注意,当使用单类型二元运算符进行混合类型算术运算时,类型U
参数必须转换为类型T
。但是,在实践中,通常存在更有效的实现,例如T::operator-=(const U&)
,可以避免从U
到T
的不必要转换。
算术运算符的两个参数模板形式创建了使用这些更有效实现的附加运算符接口。但是,“左”形式没有性能提升:它们仍然需要从U
到T
的转换,并且具有等效于编译器如果认为单类型二元运算符是最匹配的运算符而自动生成的代码的实现。
除算术示例和迭代器帮助器之外的每个运算符类模板都有一个额外的(但可选的)模板类型参数B
。此参数将是已实例化模板的公开派生基类。这意味着它必须是类类型。它可以用于避免通常与从多个空基类进行多重继承相关的对象大小膨胀。有关详细信息,请参阅旧版本用户的说明。
要提供对一组运算符的支持,请使用B
参数将运算符模板链接到单个基类层次结构中,如用法示例中所示。复合运算符模板也使用此技术来分组运算符定义。如果链对于编译器来说太长而无法支持,请尝试用一个将旧模板链接在一起的单个分组运算符模板替换一些运算符模板;长度限制仅适用于链中直接存在的模板数量,而不适用于隐藏在组模板中的模板。
警告:当使用Boost运算符模板的单参数形式链接到不是Boost运算符模板的基类时,必须使用尾随'1'
指定运算符模板。否则,库将假设您打算定义一个结合您打算用作基类的类和您正在派生的类的二元运算。
在某些编译器(例如Borland、GCC)上,即使是单继承在某些情况下似乎也会导致对象大小增加。如果您没有定义类模板,则可以通过完全避免派生并改用以下方式显式实例化运算符模板来获得更好的对象大小性能。
class my_class // lose the inheritance... { //... }; // explicitly instantiate the operators I need. template struct less_than_comparable<my_class>; template struct equality_comparable<my_class>; template struct incrementable<my_class>; template struct decrementable<my_class>; template struct addable<my_class,long>; template struct subtractable<my_class,long>;
请注意,某些运算符模板无法使用此解决方法,并且必须是其主要操作数类型的基类。这些模板定义的运算符必须是成员函数,而解决方法需要运算符是独立的friend
函数。相关的模板是
dereferenceable<>
indexable<>
正如Daniel Krugler指出的那样,此技术违反了C++11 §14.6.5/2 [temp.inject],因此不可移植。原因是,例如less_than_comparable<my_class>
的实例化注入的运算符无法根据C++11 §3.4.2/2 [basic.lookup.argdep]中给出的规则由ADL找到,因为my_class
不是less_than_comparable<my_class>
的关联类。因此,只有在其他方法都失败时才使用此技术。
许多编译器(例如MSVC 6.3、GCC 2.95.2)不会强制执行运算符模板表中的要求,除非实际使用了依赖它们的运算。这不是符合标准的行为。特别是,尽管从operators<>
和operators2<>
模板派生所有需要二元运算符的类可能很方便,而不管它们是否实现了这些模板的所有要求,但这是一种快捷方式,不可移植。即使这目前在您的编译器上有效,以后也可能无效。
算术运算符模板简化了创建自定义数值类型的任务。给定一组核心运算符,模板会向数值类添加相关的运算符。这些运算与标准算术类型具有的运算类似,可能包括比较、加法、增量、逻辑和位操作等。此外,由于大多数数值类型需要多个这样的运算符,因此提供了一些模板来在一个声明中组合几个基本运算符模板。
用于实例化简单运算符模板的类型的要求是根据必须有效的表达式和表达式的返回类型来指定的。复合运算符模板仅列出它们使用的其他模板。复合运算符模板的提供的运算和要求可以从列出的组件的运算和要求推断出来。
这些模板之所以“简单”,是因为它们基于基类型必须提供的单个运算来提供运算符。它们还有一个额外的可选模板参数B
(未显示),用于基类链技术。
主要操作数类型T
必须是类类型,不支持内置类型。
表 1.7。简单的算术运算符模板类
模板 |
提供的运算 |
需求 |
传播 constexpr |
---|---|---|---|
|
返回可转换为 |
自 |
|
|
|
返回可转换为 |
自 |
|
返回可转换为 |
自 |
|
|
|
返回可转换为 |
自 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
返回可转换为 |
否 |
|
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
返回可转换为 |
否 |
|
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
返回可转换为 |
否 |
|
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
否 |
|
|
返回可转换为 |
自 |
|
|
返回可转换为 |
自 |
|
返回可转换为 |
自 |
|
|
|
返回可转换为 |
自 |
排序说明:less_than_comparable<T>
和 partially_ordered<T>
模板提供了相同的操作集。但是,less_than_comparable<T>
的工作方式假设类型 T
的所有值都可以放在全序中。如果情况并非如此(例如,IEEE 浮点运算中的非数字值),则应使用 partially_ordered<T>
。 partially_ordered<T>
模板可以用于全序类型,但它不如 less_than_comparable<T>
高效。此规则也适用于 less_than_comparable<T, U>
和 partially_ordered<T,U>
(关于所有 T
和 U
值的排序),以及两个版本的 equivalent<>
。对于 equivalent<>
的解决方案是为目标类编写自定义 operator==
。
对称性说明:在讨论对称性之前,我们需要讨论优化,以了解运算符不同实现风格的原因。让我们以类 T
的 operator+
为例
T operator+( const T& lhs, const T& rhs ) { return T( lhs ) += rhs; }
这将是 operator+
的正常实现,但它不是高效的实现。创建了 lhs
的未命名的局部副本,在其上调用 operator+=
,并将其复制到函数返回值(这是另一个未命名的 T
类型对象)。标准通常不允许优化掉中间对象。
C++11 §3.7.3/3 [basic.stc.auto]:自动存储持续时间:如果具有自动存储持续时间的变量具有初始化或具有副作用的析构函数,则在块结束之前不得销毁它,也不得将其消除作为优化,即使它似乎未被使用,除非类对象或其副本/移动可以按照 12.8 中指定的进行消除。
对 §12.8 的引用对我们很重要。
C++11 §12.8/31 [class.copy]:复制和移动类对象:满足某些条件时,即使对象的复制/移动构造函数和/或析构函数具有副作用,实现也可以省略类对象的复制/移动构造。(...)这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的(可以组合起来消除多个副本)
— 在具有类返回类型的函数的
return
语句中,当表达式是非易失性自动对象的名称(函数或捕获子句参数除外)并且具有与函数返回类型相同的 cv 无限定类型时,可以通过直接构造自动对象到函数的返回值来省略复制/移动操作。(…)
这种优化被称为命名返回值优化(NRVO),这导致了以下 operator+
的实现。
T operator+( const T& lhs, const T& rhs ) { T nrv( lhs ); nrv += rhs; return nrv; }
有了这个实现,编译器就可以删除中间对象了。遗憾的是,并非所有编译器都实现了 NRVO,有些编译器甚至以错误的方式实现它,这使得它在这里毫无用处。如果没有 NRVO,则 NRVO 友好的代码不会比上面显示的原始代码更差,但是还有另一种可能的实现,它具有一些非常特殊的属性。
T operator+( T lhs, const T& rhs ) { return lhs += rhs; }
与第一个实现的区别在于 lhs
不是作为用于创建副本的常量引用;相反,lhs
是按值参数,因此它已经是所需的副本。这允许对某些情况进行另一种优化(C++11 §12.2/2 [class.temporary])。
考虑 a + b + c
,其中当在添加 c
时用作 lhs
时,不会复制 a + b
的结果。这比原始代码更有效,但不如使用 NRVO 的编译器高效。对于大多数人来说,对于没有实现 NRVO 的编译器,它仍然是首选,但是 operator+
现在具有不同的函数签名。此外,对于 (a + b) + c
和 a + (b + c)
,创建的对象数量也不同。
很可能,这不会成为你的问题,但是如果你的代码依赖于函数签名或严格的对称行为,你应该在你的用户配置中设置 BOOST_FORCE_SYMMETRIC_OPERATORS
。这将强制使用 NRVO 友好的实现,即使对于没有实现 NRVO 的编译器也是如此。
以下模板提供了常见的一组相关操作。例如,由于可加类型通常也是可减类型,因此 additive
模板提供了两者的组合运算符。分组运算符模板有一个额外的可选模板参数 B
(未显示),用于 基类链接 技术。
表 1.9. 分组算术运算符模板类
模板 |
组件运算符模板 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
拼写:euclidean vs. euclidian: Boost.Operators库的旧版本使用"euclidian
",但指出"euclidean
"是更常见的拼写。为了与旧版本兼容,该库现在支持两种拼写。
算术运算符类模板operators<>
和operators2<>
是非可扩展运算符分组类的示例。这些来自先前版本头文件的旧类模板不能用于基类链式调用。
算术运算符演示和测试程序: operators_test.cpp
程序演示了算术运算符模板的使用,也可以用来验证其正确操作。请检查编译器状态报告以了解所选平台的测试结果。
迭代器辅助函数 模板简化了创建自定义迭代器的任务。与算术类型类似,完整的迭代器具有许多“冗余”的运算符,这些运算符可以用核心运算符集来实现。
解引用运算符 的设计动机是 迭代器辅助函数,但它们在非迭代器上下文中也很有用。许多冗余的迭代器运算符也是算术运算符,因此迭代器辅助类借用了上面定义的许多运算符。实际上,只需要定义两个新运算符:指向成员的operator->
和下标operator[]
。
用于实例化解引用运算符的类型的要求是用必须有效的表达式及其返回类型来指定的。复合运算符模板列出了其组件模板(实例化类型必须支持),以及其他可能的要求。
此表中的所有解引用运算符模板都接受一个可选的模板参数(未显示),用于 基类链接。
表 1.12. 符号
键 |
描述 |
---|---|
|
操作数类型 |
|
|
|
类型为 |
|
|
|
|
|
类型为 |
表 1.13. 解引用运算符模板类
模板 |
提供的运算 |
需求 |
---|---|---|
|
|
|
|
|
|
有五个迭代器运算符类模板,每个模板对应不同的迭代器类别。下表显示了自定义迭代器可以定义的任何类别的运算符组。这些类模板还有一个额外的可选模板参数 B
(未显示),用于支持 基类链接。
表 1.15. 迭代器运算符类模板
模板 |
组件运算符模板 |
---|---|
|
|
|
|
|
也有五个迭代器辅助类模板,每个模板对应一个不同的迭代器类别。这些类不能用于 基类链接。以下摘要显示这些类模板同时提供了来自 迭代器运算符类模板 的迭代器运算符和 C++ 标准所需的迭代器 typedef
,例如 iterator_category
和 value_type
。
表 1.17. 辅助类模板
模板 |
操作和要求 |
---|---|
|
支持操作并具有 |
支持操作并具有 |
|
|
支持操作并具有 |
|
支持操作并具有 |
|
支持操作并具有 为了满足 RandomAccessIterator,还需要 |
output_iterator_helper
只接受一个模板参数——其目标类的类型。虽然有些人可能认为这是一个不必要的限制,但标准要求任何输出迭代器的 difference_type
和 value_type
为 void
(C++11 §24.4.1 [lib.iterator.traits]),而 output_iterator_helper
模板遵守此要求。此外,标准中的输出迭代器具有 void pointer
和 reference
类型,因此 output_iterator_helper
也这样做。output_iterator_helper
通过定义 operator*
和 operator++
成员函数来支持这种习惯用法,这些函数只返回对迭代器本身的非常量引用。对自代理的支持使我们能够在许多情况下,将编写输出迭代器的任务简化为仅编写两个成员函数——一个合适的构造函数和一个复制赋值运算符。例如,以下是 boost::function_output_iterator
适配器的可能实现template<class UnaryFunction>
struct function_output_iterator
: boost::output_iterator_helper
< function_output_iterator<UnaryFunction> >
{
explicit function_output_iterator(UnaryFunction const& f = UnaryFunction())
: func(f) {}
template<typename T>
function_output_iterator& operator=(T const& value)
{
this->func(value);
return *this;
}
private:
UnaryFunction func;
};
请注意,对自代理的支持不会阻止您使用 output_iterator_helper
来简化任何其他不同类型的输出迭代器的实现。如果 output_iterator_helper
的目标类型提供了它自己的 operator*
或/和 operator++
定义,那么这些运算符将被使用,而 output_iterator_helper
提供的运算符将永远不会被实例化。
iterators_test.cpp
程序演示了迭代器模板的使用,也可以用于验证正确的操作。以下是测试程序中定义的自定义迭代器。它演示了必须定义以使迭代器辅助程序“填充”其余迭代器操作的核心操作的正确(虽然很简单)实现。
template <class T, class R, class P> struct test_iter : public boost::random_access_iterator_helper
< test_iter<T, R, P>, T,std::ptrdiff_t
, P, R > { typedef test_iter self; typedef R Reference; typedefstd::ptrdiff_t
Distance; public: explicit test_iter(T* i =0); test_iter(const self& x); self& operator=(const self& x); Reference operator*() const; self& operator++(); self& operator--(); self& operator+=(Distance n); self& operator-=(Distance n); bool operator==(const self& x) const; bool operator<(const self& x) const; friend Distance operator-(const self& x, const self& y); };
查看 编译器状态报告 以了解所选平台的测试结果。
库接口和推荐用法的更改 是由下面描述的一些实际问题引起的。新版本的库仍然与旧版本向后兼容,因此您不必强制更改任何现有代码,但旧用法已弃用。
虽然它可以说是比使用基类链 更简单、更直观,但已发现从多个运算符模板派生的旧做法会导致生成的类比应有的类大得多。大多数现代 C++ 编译器会显著增加从多个空基类派生的类的规模,即使基类本身没有任何状态。例如,来自上面示例的point<int>
在各种 Win32 平台编译器上的大小为 12-24 字节,而不是预期的 8 字节。
严格来说,这不是库的错误——语言规则允许编译器在这种情况下应用空基类优化。原则上,可以将任意数量的空基类分配到相同的偏移量,前提是它们没有公共祖先(参见 C++11 标准的 §10 [class.derived] 第 8 段)。
但是,语言定义也没有要求实现进行优化,并且当今几乎没有编译器在涉及多重继承时实现它。更糟糕的是,实现者不太可能将其作为对现有编译器的未来增强来采用,因为它会破坏由同一编译器的两个不同版本生成的代码之间的二进制兼容性。正如 Matt Austern 所说,“当您面向新的架构时……这是您拥有这种自由的少数几次机会之一”。另一方面,许多常用编译器将对单继承层次结构使用空基类优化。
鉴于这个问题对库用户的重要性(该库旨在用于编写轻量级类,如MyInt
或point<>
),以及上述因素,我们决定更改库接口,以便即使在仅支持空基类优化的最简单形式的编译器上也能消除对象大小膨胀。当前的库接口是这些更改的结果。虽然新用法比旧用法复杂一些,但我们认为为了使库在现实世界中更有用,这是值得的。Alexy Gurtovoy 贡献了支持新用法习惯用法的代码,同时允许库保持向后兼容性。
boost/operators.hpp
中贡献了算术运算符。operators_test.cpp
。