头文件 <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+
。
双参数形式 的模板也提供用于允许与其他类型交互。
关于概念使用的说明:讨论的概念不一定是标准库的概念,例如 CopyConstructible,尽管其中一些可能是;它们是我们称之为小写“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],因此不具有可移植性。原因是,根据 C++11 §3.4.2/2 [basic.lookup.argdep] 给出的规则,ADL 无法找到通过实例化例如 less_than_comparable<my_class>
注入的运算符,因为 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- 非限定类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作(…)
这种优化被称为命名返回值优化 (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
,其中当 a + b
的结果用作添加 c
时的 lhs
时,不会复制。 这比原始代码更有效,但不如使用 NRVO 的编译器有效。 对于大多数人来说,对于不实现 NRVO 的编译器来说,它仍然是更可取的,但是 operator+
现在具有不同的函数签名。 此外,对于 (a + b) + c
和 a + (b + c)
,创建的对象数量也不同。
很可能,这对您来说不会成为问题,但如果您的代码依赖于函数签名或严格的对称行为,则应在您的 user-config 中设置 BOOST_FORCE_SYMMETRIC_OPERATORS
。 这将强制即使对于不实现 NRVO 的编译器也使用 NRVO 友好的实现。
以下模板提供相关操作的常用组。 例如,由于可加类型通常也是可减类型,因此 additive
模板提供了两者的组合运算符。 分组运算符模板有一个额外的可选模板参数 B
,该参数未显示,用于基类链接技术。
表 1.9. 分组算术运算符模板类
模板 |
组件运算符模板 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
拼写:euclidean 与 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++ 编译器会显著膨胀从多个空基类派生的类的大小,即使基类本身没有状态。例如,在 Win32 平台上的各种编译器上,上面示例中的 point<int>
的大小为 12-24 字节,而不是预期的 8 字节。
严格来说,这不是库的错——语言规则允许编译器在这种情况下应用空基类优化。原则上,任意数量的空基类可以分配在相同的偏移量,前提是它们都没有共同的祖先(请参阅 C++11 标准的 §10 [class.derived] 第 8 段)。
但是语言定义也 **不** 要求 实现执行优化,而且当涉及多重继承时,当今几乎没有编译器实现它。更糟糕的是,实现者不太可能将其作为对现有编译器的未来增强功能采用,因为它会破坏同一编译器的两个不同版本生成的代码之间的二进制兼容性。正如 Matt Austern 所说,“您有自由做这类事情的少数几次之一是当您以新架构为目标时……”。另一方面,许多常见的编译器将对单继承层次结构使用空基类优化。
鉴于这个问题对于库的用户的重要性(该库旨在用于编写轻量级类,如 MyInt
或 point<>
),以及上述原因,我们决定更改库接口,以便即使在仅支持最简单的空基类优化形式的编译器上,也可以消除对象大小膨胀。当前的库接口是这些更改的结果。尽管新用法比旧用法稍微复杂一些,但我们认为为了使库在现实世界中更有用,这是值得的。Alexy Gurtovoy 贡献了支持新用法习惯的代码,同时允许库保持向后兼容。
boost/operators.hpp
中贡献了算术运算符。operators_test.cpp
。