Идиоматическое использование std :: rel_ops - PullRequest
45 голосов
/ 03 июня 2011

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

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

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

Ответы [ 4 ]

32 голосов
/ 14 февраля 2014

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

24 голосов
/ 03 июня 2011

Я думаю, что предпочтительный метод не использовать 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;
}
2 голосов
/ 01 мая 2018

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

Использование 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 (не показано ниже). </p>

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

2 голосов
/ 08 сентября 2017

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

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

...