习惯使用std :: rel_ops

时间:2011-06-03 09:38:58

标签: c++ c++-standard-library idioms

使用std::rel_ops将完整的关系运算符集添加到类中的首选方法是什么?

This文档建议使用using namespace std::rel_ops,但这似乎存在严重缺陷,因为这意味着包含以这种方式实现的类的标头也会为所有其他类添加完整的关系运算符使用已定义的operator<operator==,即使不是这样。这有可能以令人惊讶的方式改变代码的含义。

作为旁注 - 我一直在使用Boost.Operators来做这件事,但我仍然对标准库感到好奇。

4 个答案:

答案 0 :(得分:31)

运算符重载用户定义类的方式是通过参数依赖查找来实现的。 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 无关紧要,并且只要在使用运算符重载之前将标题包含在某个时间中,那么ADL将起作用。规范当然没有这样的保证,并且实际的实施情况并非如此。

使用libc ++,由于libc ++使用内联命名空间,(IIUC)会认为make_rel_ops_work的声明与包含<utility>运算符的命名空间位于不同的命名空间中,除非{{1} }} {}}的声明是第一位的。这是因为,从技术上讲,即使<utility>是内联命名空间,std::rel_opsstd::__1::rel_ops也是不同的命名空间。但是如果clang看到std::rel_ops的原始名称空间声明位于内联名称空间std::__1中,那么它会将rel_ops声明视为扩展__1而不是新名称空间

我相信这个命名空间扩展行为是一个clang扩展而不是C ++指定的,所以你可能甚至无法在其他实现中依赖它。特别是gcc不会这样,但幸运的是libstdc ++不使用内联命名空间。如果您不想依赖此扩展,那么对于clang / libc ++,您可以编写:

namespace std { namespace rel_ops {

但显然你需要为你使用的其他库实现。我更简单的std::__1::rel_ops声明适用于clang3.2 / libc ++,gcc4.7.3 / libstdc ++和VS2012。

答案 1 :(得分:23)

我认为首选技术不是使用std::rel_ops 所有。 boost::operatorlink)中使用的技术似乎是常见的 溶液

示例:

#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;
}

答案 2 :(得分:1)

这不是最好的,但您可以使用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中定义的那个。

这仍然是一个痛苦,因为你仍然需要为每个比较运算符编写一个函数。

答案 3 :(得分:1)

添加rel_ops命名空间的问题,无论您是使用手册using namespace rel_ops;执行还是自动执行,如@bames53的回答所述,添加命名空间可能会产生意外的副作用部分代码。我最近才发现这一点,因为我一直在使用@ bames53解决方案一段时间,但是当我更改了一个基于容器的操作以使用reverse_iterator而不是迭代器时(在多图中,但我怀疑它会是相同的)任何标准容器),突然间我在使用!=比较两个迭代器时遇到了编译错误。最后,我追溯到这样一个事实:代码包含了rel_ops命名空间,这干扰了reverse_iterators的定义方式。

使用boost可以解决它,但正如@Tom所提到的,并非所有人都愿意使用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,你可以从这个继承来添加“缺失”的运算符。当然你需要定义==和&lt; MyClass中的运算符(未在下面显示)。

class MyClass : public add_rel_ops<MyClass> {
    ...stuff...
};

包含MyClass作为模板参数非常重要。如果您要包含其他课程,请说MyOtherClassstatic_cast几乎肯定会给您带来问题。

请注意,我的解决方案是假设==<运算符定义为const noexcept,这是我个人编码标准的要求之一。如果您的标准不同,则需要相应地修改add_rel_ops。

此外,如果您对使用static_cast感到困扰,可以通过添加

将其更改为dynamic_cast
virtual ~add_rel_ops() noexcept = default;

到add_rel_ops类以使其成为虚拟类。当然,这也会迫使MyClass成为一个虚拟类,这就是我不接受这种方法的原因。