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

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

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

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


person Mankarse    schedule 03.06.2011    source источник
comment
Другая проблема с using namespace std::rel_ops заключается в том, что операторы не учитываются при поиске, зависящем от аргументов. Это означает, что, например, std::greater<my_type> не удастся скомпилировать (в то время как он был бы успешным, если бы подходящий operator> был определен в том же пространстве имен, что и my_type, или в глобальном пространстве имен).   -  person Mike Seymour    schedule 03.06.2011
comment
@MikeSeymour Я добавил решение (не переносимое по спецификации, но довольно переносимое на практике), которое заставляет ADL работать с rel_ops.   -  person bames53    schedule 14.02.2014
comment
Он устарел в C++20   -  person 陳 力    schedule 22.11.2018
comment
... в пользу оператора космического корабля <=>.   -  person L. F.    schedule 06.06.2019


Ответы (4)


Я думаю, что предпочтительнее вообще не использовать std::rel_ops. Техника, используемая в 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;
}
person James Kanze    schedule 03.06.2011
comment
На самом деле это не отвечает на вопрос. Многие проекты разработки C++ не включают ускорение из-за его размера и сложности. - person Tom Swirly; 16.02.2016
comment
Ответ можно расширить, чтобы, знаете ли, описать технику, использованную в boost::operator. В общих чертах, хотя ответ правильный в том смысле, что идиоматический (или любой другой) способ использования std::rel_ops заключается в том, чтобы просто не делать этого. Даже свертывание собственного шаблона класса с операторами лучше. - person Luis Machuca; 13.03.2016

Предполагалось, что перегрузки операторов для пользовательских классов будут работать через поиск, зависящий от аргумента. 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 будет работать. Спецификация, конечно, не дает такой гарантии, и есть реальные реализации, где это не так.

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.

person bames53    schedule 13.02.2014
comment
Или вы можете просто написать свою собственную версию std::rel_ops (возможно, реализованную путем возврата к std::rel_ops) и избежать UB. - person Justin; 23.12.2017

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

person Steven W. Klassen    schedule 01.05.2018

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

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

person Justin    schedule 08.09.2017
comment
Какое преимущество это дает по сравнению с написанием перегрузки вручную? - person Yashas; 07.04.2019
comment
@Yashas Если T не имеет operator<=, using namespace std::rel_ops означает, что operator<= все еще работает, при условии, что T имеет некоторые операторы сравнения. - person Justin; 08.04.2019
comment
@ Джастин, я предпочитаю return !(rhs < lhs). - person L. F.; 06.06.2019
comment
@ Л.Ф. Я тоже, но преимущество использования std::rel_ops в том, что он вызовет lhs <= rhs, если он существует, но в противном случае вернется к !(rhs < lhs) - person Justin; 06.06.2019