std::rel_ops 的惯用用法
Posted
技术标签:
【中文标题】std::rel_ops 的惯用用法【英文标题】:Idiomatic use of std::rel_ops 【发布时间】:2011-09-07 16:43:40 【问题描述】:使用std::rel_ops
将全套关系运算符添加到类的首选方法是什么?
This 文档建议使用using namespace std::rel_ops
,但这似乎存在严重缺陷,因为这意味着包含以这种方式实现的类的标头也会将完整的关系运算符添加到具有定义的所有其他类 @ 987654325@ 和 operator==
,即使这是不希望的。这有可能以惊人的方式改变代码的含义。
附带说明 - 我一直在使用 Boost.Operators 来执行此操作,但我仍然对标准库感到好奇。
【问题讨论】:
using namespace std::rel_ops
的另一个问题是运算符不被考虑用于依赖于参数的查找。这意味着,例如,std::greater<my_type>
将无法编译(而如果在与 my_type
相同的命名空间或全局命名空间中定义了合适的 operator>
,它将成功)。
@MikeSeymour 我添加了一个(非便携规范,但在实践中相当便携)解决方案,使 ADL 与 rel_ops 一起工作。
在 C++20 中已被弃用
...赞成宇宙飞船操作员<=>
。
【参考方案1】:
用户定义类的运算符重载的工作方式是通过参数相关查找。 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>
之后。有人可能会天真地认为,在 here 中包含这个并不重要,只要在使用运算符重载之前 sometime 包含标题,那么 ADL 就可以工作。规范当然没有这样的保证,并且在实际实现中并非如此。
clang 与 libc++,由于 libc++ 使用内联命名空间,将 (IIUC) 认为 make_rel_ops_work
的声明与包含 <utility>
运算符重载的命名空间不同的命名空间,除非 <utility>
的声明std::rel_ops
是第一位的。这是因为,从技术上讲,std::__1::rel_ops
和 std::rel_ops
是不同的命名空间,即使 std::__1
是一个内联命名空间。但如果 clang 发现 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
版本(可能通过使用std::rel_ops
来实现)并避免UB【参考方案2】:
我认为首选的技术是不要使用std::rel_ops
at
全部。 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;
【讨论】:
这实际上并没有回答问题。由于其规模和复杂性,许多 C++ 开发项目不包括 boost。 答案可以扩展到,你知道,描述boost::operator
中使用的技术。一般而言,尽管答案是正确的,因为使用std::rel_ops
的惯用(或其他任何方式)方式就是不这样做。甚至使用运算符滚动您自己的类模板也更好。【参考方案3】:
添加 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
几乎肯定会给你带来问题。
请注意,我的解决方案是假设==
和<
运算符定义为const noexcept
,这是我个人编码标准的要求之一。如果您的标准不同,则需要相应地修改 add_rel_ops。
另外,如果您对static_cast
的使用感到困扰,您可以通过添加将它们更改为dynamic_cast
virtual ~add_rel_ops() noexcept = default;
添加到 add_rel_ops 类以使其成为虚拟类。当然,这也会迫使MyClass
成为一个虚拟类,这就是我不采用这种方法的原因。
【讨论】:
【参考方案4】:这不是最好的,但您可以使用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;
,如果为该类型定义了operator<=
,我们允许ADL 查找,否则回退到std::rel_ops
中定义的那个。
不过,这仍然很痛苦,因为您仍然需要为每个比较运算符编写一个函数。
【讨论】:
这比手动编写重载有什么优势? @Yashas 如果T
没有operator<=
,using namespace std::rel_ops
表示operator<=
仍然有效,前提是T
确实有一些比较运算符。
@Justin 我更喜欢return !(rhs < lhs)
。
@L.F.我也是,但是像这样使用std::rel_ops
的好处是它会调用lhs <= rhs
(如果存在),否则会退回到!(rhs < lhs)
以上是关于std::rel_ops 的惯用用法的主要内容,如果未能解决你的问题,请参考以下文章
零基础入门学习Python,这13个Python惯用小技巧一定要收藏