Идиоматическое использование std:: rel ops

каков предпочтительный метод использования std::rel_ops добавить полный набор реляционных операторов в класс?

этой документация предполагает using namespace std::rel_ops, но это кажется глубоко ошибочным, так как это означало бы, что включение заголовка для класса, реализованного таким образом, также добавит полные реляционные операторы ко всем другим классам с определенным operator< и operator==, даже если это было нежелательно. Это может изменить значение кода в удивительный способ.

в качестве примечания-я использовал импульс.Операторы чтобы сделать это, но мне все еще интересно о стандартной библиотеке.

4 ответов


Я думаю, что выбранный метод не использовать std::rel_ops at все. Техника, используемая в boost::operator (ссылке) кажется, как обычно решение.

пример:

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

перегрузка оператора для пользовательских классов должна была работать через поиск, зависящий от аргумента. ADL позволяет программам и библиотекам избегать загромождения глобального пространства имен перегрузками операторов, но все же позволяет удобно использовать операторы; то есть без явной квалификации пространства имен, что невозможно сделать с синтаксисом оператора infix a + b и вместо этого потребуется обычный синтаксис функции your_namespace::operator+ (a, b).

ADL, однако, не просто ищет везде для любой возможной перегрузки оператора. ADL ограничен для просмотра только "связанных" классов и пространств имен. Проблема с std::rel_ops это, как указано, это пространство имен Никогда не может быть связанным пространством имен любого класса, определенного вне стандартной библиотеки, и поэтому ADL не может работать с такими пользовательскими типами.

однако, если вы готовы обмануть вы можете сделать std::rel_ops работа.

связанные пространства имен, определенные в C++11 3.4.2 [основной.уважать.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. Спецификация, конечно, не дает такой гарантии, и есть фактические реализации, где это не так.

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.


это не самое приятное, но вы можете использовать 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 иначе.

это все еще боль, хотя, как вы все еще должны написать функцию для каждого из операторов сравнения.


проблема с добавлением пространства имен rel_ops, независимо от того, делаете ли вы это с помощью руководства using namespace rel_ops; или вы делаете это автоматически, как описано в ответе @bames53, что добавление пространства имен может иметь непредвиденные побочные эффекты на части вашего кода. Я нашел это сам совсем недавно, так как я использовал решение @bames53 в течение некоторого времени, но когда я изменил одну из моих операций на основе контейнера, чтобы использовать reverse_iterator вместо итератора (в multimap, но я подозреваю, что это будет то же самое для любого из стандартных контейнеров), вдруг я получаю ошибки компиляции при использовании != для сравнения двух итераторов. В конечном итоге я отследил его до того, что код включал пространство имен 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, вы можете наследовать от этого, чтобы добавить "недостающие" операторов. Конечно, вам нужно определить операторы == и

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

важно, чтобы вы включили MyClass в качестве аргумента шаблона. Если бы вы включили другой класс, скажем MyOtherClass, the static_cast было бы почти наверняка дать вам проблемы.

обратите внимание, что мое решение предполагает, что == и < операторы определяются как const noexcept что является одним из требований моих личных стандартов кодирования. Если ваши стандарты отличаются, вам нужно будет соответствующим образом изменить add_rel_ops.

кроме того, если вас беспокоит использование static_cast, вы можете изменить их, чтобы быть dynamic_cast by добавление

virtual ~add_rel_ops() noexcept = default;

в класс add_rel_ops, чтобы сделать его виртуальным классом. Конечно, это тоже заставит MyClass быть виртуальным классом, поэтому я не использую этот подход.