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

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

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

В качестве побочного примечания - я использовал Boost.Operators, но мне все еще интересно узнать о стандартной библиотеке.

Ответ 1

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

Ответ 2

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

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

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

Связанные пространства имен определены в С++ 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 самостоятельно технически заставляет программу иметь поведение undefined, потому что С++ не позволяет пользовательским программам добавлять объявления к std. В качестве примера того, как это действительно имеет значение, и почему, если вы это сделаете, вы можете столкнуться с проблемой проверки того, что ваша реализация действительно работает с этим дополнением, подумайте:

Выше показано объявление make_rel_ops_work, которое следует за #include <utility>. Можно было бы наивно ожидать, что включение здесь здесь не имеет значения и что до тех пор, пока заголовок будет включен когда-то до использования перегрузок оператора, ADL будет работать. Спецификация, конечно, не дает такой гарантии, и есть реальные реализации, когда это не так.

clang с libС++, из-за использования внутренних пространств имен libС++, будет (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, а не задано С++, поэтому вы даже не можете полагаться на это в других реализациях. В частности, gcc не ведет себя так, но, к счастью, libstdС++ не использует встроенные пространства имен. Если вы не хотите полагаться на это расширение, то для clang/libС++ вы можете написать:

#include <__config>
_LIBCPP_BEGIN_NAMESPACE_STD
namespace rel_ops { struct make_rel_ops_work {}; }
_LIBCPP_END_NAMESPACE_STD

но, очевидно, вам понадобятся реализации для других библиотек, которые вы используете. Мое упрощенное объявление make_rel_ops_work работает для clang3.2/libС++, gcc4.7.3/libstdС++ и VS2012.

Ответ 3

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

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

Ответ 4

Проблема с добавлением пространства имен rel_ops, независимо от того, выполняете ли вы это с помощью руководства using namespace rel_ops; или выполняете ли вы это автоматически, как описано в ответе @bames53, заключается в том, что добавление пространства имен может иметь непредвиденные побочные эффекты на участках ваш код. Я нашел это сам совсем недавно, поскольку некоторое время я использовал решение @bames53, но когда я сменил одну из своих операций на основе контейнера, чтобы использовать обратный_запись вместо итератора (внутри мультимапа, но я подозреваю, что это будет одинаково для любой из стандартных контейнеров), внезапно я получал ошибки компиляции при использовании!= для сравнения двух итераторов. В конечном счете я отследил его до того, что код включал пространство имен rel_ops, которое мешало тому, как определяются обратные_тераторы.

Использование boost было бы способом его решения, но, как упоминалось в @Tom, не все готовы использовать 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 быть виртуальным классом, поэтому я не принимаю этот подход.