std::rel_ops
将全套关系运算符添加到类中的首选方法是什么?
该文档建议使用using namespace std::rel_ops
,但这似乎存在严重缺陷,因为这意味着包含以这种方式实现的类的标头也会将完整的关系运算符添加到具有定义的所有其他类中operator<
,operator==
即使不希望这样做。这有可能以惊人的方式改变代码的含义。
作为旁注 - 我一直在使用Boost.Operators来执行此操作,但我仍然对标准库感到好奇。
std::rel_ops
将全套关系运算符添加到类中的首选方法是什么?
该文档建议使用using namespace std::rel_ops
,但这似乎存在严重缺陷,因为这意味着包含以这种方式实现的类的标头也会将完整的关系运算符添加到具有定义的所有其他类中operator<
,operator==
即使不希望这样做。这有可能以惊人的方式改变代码的含义。
作为旁注 - 我一直在使用Boost.Operators来执行此操作,但我仍然对标准库感到好奇。
用户定义类的运算符重载的工作方式是通过参数相关查找。ADL 允许程序和库避免因运算符重载而弄乱全局命名空间,但仍允许方便地使用运算符;也就是说,如果没有明确的命名空间限定,这与中缀运算符语法无关,a + b
而是需要正常的函数语法your_namespace::operator+ (a, b)
。
然而,ADL 并不只是到处搜索任何可能的运算符重载。ADL 仅限于查看“关联”类和命名空间。问题std::rel_ops
在于,正如指定的那样,这个命名空间永远不能是标准库之外定义的任何类的关联命名空间,因此 ADL 不能与此类用户定义类型一起使用。
但是,如果您愿意作弊,则可以std::rel_ops
工作。
关联的命名空间在 C++11 3.4.2 [basic.lookup.argdep] /2 中定义。对于我们的目的,重要的事实是基类是其成员的命名空间是继承类的关联命名空间,因此 ADL 将检查这些命名空间是否有适当的功能。
所以,如果出现以下情况:
#include <utility> // rel_ops
namespace std { namespace rel_ops { struct make_rel_ops_work {}; } }
将(以某种方式)找到进入翻译单元的方式,然后在支持的实现(见下一节)上,您可以定义自己的类类型,如下所示:
namespace N {
// inherit from make_rel_ops_work so that std::rel_ops is an associated namespace for ADL
struct S : private std::rel_ops::make_rel_ops_work {};
bool operator== (S const &lhs, S const &rhs) { return true; }
bool operator< (S const &lhs, S const &rhs) { return false; }
}
然后 ADL 将适用于您的类类型,并会在std::rel_ops
.
#include "S.h"
#include <functional> // greater
int main()
{
N::S a, b;
a >= b; // okay
std::greater<N::s>()(a, b); // okay
}
当然,make_rel_ops_work
从技术上讲,添加自己会导致程序具有未定义的行为,因为 C++ 不允许用户程序将声明添加到std
. 作为一个例子,说明这实际上是如何起作用的以及为什么这样做,如果你这样做,你可能想去验证你的实现确实在这个添加下正常工作,考虑:
上面我显示了make_rel_ops_work
以下声明#include <utility>
。有人可能会天真地认为,在此处包含这个并不重要,只要在使用运算符重载之前的某个时间包含标头,那么 ADL 就可以工作。规范当然没有这样的保证,并且在实际实现中并非如此。
由于 libc++ 使用内联命名空间,与 libc++ 的 clang 将 (IIUC) 认为该声明位于make_rel_ops_work
与包含运算符重载的命名空间不同的命名空间中,<utility>
除非<utility>
的声明std::rel_ops
首先出现。这是因为,从技术上讲,即使是内联命名空间,它们也是不同std::__1::rel_ops
的命名空间。但是,如果 clang 发现原始命名空间声明位于内联命名空间中,那么它会将声明视为扩展而不是新命名空间。std::rel_ops
std::__1
rel_ops
__1
namespace std { namespace rel_ops {
std::__1::rel_ops
我相信这个命名空间扩展行为是一个 clang 扩展而不是 C++ 指定的,所以你甚至可能无法在其他实现中依赖它。特别是 gcc 不会以这种方式运行,但幸运的是 libstdc++ 不使用内联命名空间。如果您不想依赖此扩展,那么对于 clang/libc++,您可以编写:
#include <__config>
_LIBCPP_BEGIN_NAMESPACE_STD
namespace rel_ops { struct make_rel_ops_work {}; }
_LIBCPP_END_NAMESPACE_STD
但很明显,您将需要您使用的其他库的实现。我make_rel_ops_work
对 clang3.2/libc++、gcc4.7.3/libstdc++ 和 VS2012 的更简单的工作声明。
我认为首选技术是根本不使用std::rel_ops
。boost::operator
( link )中使用的技术似乎是通常的解决方案。
例子:
#include "boost/operators.hpp"
class SomeClass : private boost::equivalent<SomeClass>, boost::totally_ordered<SomeClass>
{
public:
bool operator<(const SomeClass &rhs) const
{
return someNumber < rhs.someNumber;
}
private:
int someNumber;
};
int main()
{
SomeClass a, b;
a < b;
a > b;
a <= b;
a >= b;
a == b;
a != b;
}
添加 rel_ops 命名空间的问题,无论您是手动using namespace rel_ops;
执行还是自动执行,如@bames53 的答案中所述,添加名称空间可能会对您的部分代码产生意想不到的副作用。我最近自己发现了这个问题,因为我一直在使用@bames53 解决方案一段时间,但是当我将我的一个基于容器的操作更改为使用 reverse_iterator 而不是迭代器时(在多映射中,但我怀疑它对于任何标准容器),突然我在使用 != 比较两个迭代器时遇到编译错误。最终我追查到代码包含 rel_ops 命名空间这一事实,该命名空间干扰了 reverse_iterators 的定义方式。
使用 boost 将是解决它的一种方法,但正如@Tom 所提到的,并不是每个人都愿意使用 boost,包括我自己。所以我实现了自己的类来解决问题,我怀疑这也是 boost 是如何做到的,但我没有检查 boost 库来查看。
具体来说,我定义了以下结构:
template <class T>
struct add_rel_ops {
inline bool operator!=(const T& t) const noexcept {
const T* self = static_cast<const T*>(this);
return !(*self == t);
}
inline bool operator<=(const T& t) const noexcept {
const T* self = static_cast<const T*>(this);
return (*self < t || *self == t);
}
inline bool operator>(const T& t) const noexcept {
const T* self = static_cast<const T*>(this);
return (!(*self == t) && !(*self < t));
}
inline bool operator>=(const T& t) const noexcept {
const T* self = static_cast<const T*>(this);
return !(*self < t);
}
};
要使用它,当你定义你的类时,比如 MyClass,你可以从这个继承来添加“缺失”的操作符。当然,您需要在 MyClass 中定义 == 和 < 运算符(下面未显示)。
class MyClass : public add_rel_ops<MyClass> {
...stuff...
};
包含MyClass
作为模板参数很重要。如果你要包括一个不同的类,比如说MyOtherClass
,那static_cast
几乎肯定会给你带来问题。
请注意,我的解决方案是假设==
and<
运算符被定义为const noexcept
我个人编码标准的要求之一。如果您的标准不同,则需要相应地修改 add_rel_ops。
此外,如果您对 的使用感到困扰static_cast
,您可以dynamic_cast
通过添加将它们更改为 a
virtual ~add_rel_ops() noexcept = default;
添加到 add_rel_ops 类以使其成为虚拟类。当然,这也将强制MyClass
成为一个虚拟类,这就是我不采用这种方法的原因。
这不是最好的,但您可以将using namespace std::rel_ops
其用作在您的类型上实现比较运算符的实现细节。例如:
template <typename T>
struct MyType
{
T value;
friend bool operator<(MyType const& lhs, MyType const& rhs)
{
// The type must define `operator<`; std::rel_ops doesn't do that
return lhs.value < rhs.value;
}
friend bool operator<=(MyType const& lhs, MyType const& rhs)
{
using namespace std::rel_ops;
return lhs.value <= rhs.value;
}
// ... all the other comparison operators
};
通过使用using namespace std::rel_ops;
,我们允许 ADL 查找它是否为该类型定义,否则operator<=
回退到定义的那个。std::rel_ops
但是,这仍然很痛苦,因为您仍然必须为每个比较运算符编写一个函数。