оператор‹ сравнения нескольких полей

У меня есть следующий оператор‹, который должен сортировать сначала по значению, а затем по другому значению:

    inline bool operator < (const obj& a, const obj& b) 
    {
        if(a.field1< b.field1)
            return true;
        else
            return a.field2 < b.field2;
    }

У меня такое ощущение, что это неверно и что вы не можете сделать это без еще одного третьего сравнительного теста для переменных-членов, но я не могу найти ни одного примера, где это не работает. Так что же это действительно так, как ожидалось? Благодарность

изменить: я бы закодировал его как:

    inline bool operator < (const obj& a, const obj& b) 
    {
        if(a.field1< b.field1)
            return true;
                    else if(a.field1> b.field1)
            return false;
        else
            return a.field2 < b.field2;
    }

есть ли отличия? Я спрашиваю, потому что по опыту знаю, что мой правильный, но он длиннее первого.


person lezebulon    schedule 03.07.2012    source источник
comment
Почему это не должно работать? Может быть, вам следует лучше объяснить свои чувства, а также то, что значит сортировать, как ожидалось.   -  person Daniel Daranas    schedule 03.07.2012
comment
Ваш оригинал не работает с if( obj(2,1) < obj(1,2) ).   -  person Robᵩ    schedule 03.07.2012
comment
Так это даже строгий слабый порядок? Меня бы это устроило, если бы я был хотя бы уверен, что не получаю ошибок во время выполнения.   -  person lezebulon    schedule 03.07.2012
comment
Для меня это довольно распространенная идиома — проверять каждого члена, кроме последнего, на <, затем на >, продолжая только последующие проверки в случае ==. Затем для последнего поля вас не волнует == против >, поэтому просто верните результат <. Лично я бы написал каждый оператор if в одной строке и не беспокоился бы о else, поскольку return все равно выходит из функции.   -  person Steve314    schedule 03.07.2012
comment
@Rob - Что мне не хватает? Насколько я знаю, эта функция должна быть в порядке, если она не является членом. Работа сравнений полей должна зависеть только от того, определены ли для типов полей оператор‹ и оператор›. На самом деле - исправление к моему предыдущему комментарию - я обычно проверяю (a.f1 < b.f1), а затем (b.f1 < a.f1). IIRC вы получаете автоматический operator>, когда вы определяете operator< из стандартной библиотеки, но это нормально, даже если по какой-то причине это не удается.   -  person Steve314    schedule 03.07.2012
comment
@ Steve314 Ваше решение на самом деле наиболее четко написано с использованием тернарного оператора: return a.field1 != b.field1 ? a.field1 < b.field1 : a.field2 < b.field2; (но с разрывами строк, которые я не могу добавить в комментарий).   -  person James Kanze    schedule 03.07.2012
comment
@Джеймс, я не согласен даже с двумя полями, а как насчет трех, четырех или более? Моя идиома распространяется на более крупные случаи и остается такой же удобочитаемой. Вложенные тернарные операторы очень быстро загромождаются и становятся нечитаемыми. Тернарные операторы лаконичны, но это не всегда означает, что они более удобочитаемы. Тестирование !=, а затем < может показаться более симметричным и может обеспечить более равномерную производительность (за счет замедления случая < до соответствия >), но ни один из этих вопросов не является для меня убедительным.   -  person Steve314    schedule 03.07.2012
comment
@ Steve314 Тернарный оператор делает это более понятным: тернарные операторы соединяются в цепочку так же, как ifs, поэтому нет никакой разницы в удобочитаемости. Разница в том, что при использовании тернарного оператора returns не находятся в ifs, и, поскольку есть только одна ветвь, сразу становится ясно, что нет такой ветви, где возврат был бы забыт.   -  person James Kanze    schedule 03.07.2012
comment
@James - этот отсутствующий возврат визуально сразу заметен в последовательности однострочных операторов if, особенно с вертикальным выравниванием. Я не осознавал, что тернарный оператор может сцепляться без беспорядка в скобках или риска странных ошибок, связанных с путаницей приоритетов, так что вы меня поняли.   -  person Steve314    schedule 04.07.2012
comment
@ Steve314 Steve314 Разница, по общему признанию, очень незначительна в этом случае, когда единственным содержимым ветвей if/else является возврат. (Но вам все равно нужно убедиться, что каждый if имеет else.) Что касается цепочки тернарных операторов, то это должно быть стандартной идиомой C++, которую сразу узнает любой компетентный практик. Но это не так; на самом деле, я не знаю ни одного учебника, в котором это представлено. (Джон Поттер впервые показал мне его, а я в то время использовал C++ более 10 лет.) Правильно отформатированные цепные тернарные операторы (чего я не могу сделать в комментарии) очень удобочитаемы.   -  person James Kanze    schedule 04.07.2012
comment
@James - мне не нужно проверять, что у каждого if есть else по очень простой причине - в моей идиоме вообще не используется else. Помните - я предпочитаю не зависеть от !=. Для этого у меня нет вложенных операторов if, только цепочка из них.   -  person Steve314    schedule 04.07.2012
comment
@ Steve314 Steve314 Теперь это приводит к путанице. Если что-то следует за if, я как читатель имею право предположить, что это будет выполнено.   -  person James Kanze    schedule 04.07.2012
comment
@James - не на языке семьи C, нет. Это не просто return - есть еще break, continue, throw - даже редкий частный случай, когда goto оправдано. Конечно, если бы оператор if был нетривиальным, то return стал бы скрытым выходом, и я бы с вами согласился. Но когда все тело в однострочном выражении if представляет собой либо return true;, либо return false;, нет оправдания тому, что вы этого не видите.   -  person Steve314    schedule 04.07.2012


Ответы (6)


Я бы хотел сделать все сам..

Сравнивать значения Obj::field2 следует только в том случае, если значения Obj::field1 равны.

Простой для понимания способ:

/* This will meet the requirements of Strict-Weak-Ordering */

if (a.field1 != b.field1) return a.field1 < b.field1;
else                      return a.field2 < b.field2;

Правильный (рекомендуемый) способ:

"правильный" способ реализации использует только operator< для сравнения полей, приведенное ниже выглядит сложнее, чем есть на самом деле.

Однако он даст тот же результат, что и ранее написанный простой для понимания пример.

return a.field1 < b.field1 || (
  !(b.field1 < a.field1) && a.field2 < b.field2
);

Должен ли быть способ реализовать operator<, не вызывая много головной боли?

C++11

Вы можете использовать std::tuple из STL, в котором уже есть operator< для нескольких определенных полей, например, в приведенном ниже примере.

#include <utility>

...

inline bool
operator< (Obj const& lhs, Obj const& rhs)
{
  return std::tie (lhs.field1, lhs.field2) < std::tie (rhs.field1, rhs.field);
}

C++03

Если ваш компилятор еще не поддерживает C++11 и вам нужно сравнить только два поля из каждого объекта, вы можете вместо этого использовать std::pair.

Причина для std::make_pair та же, что и в предыдущем примере с использованием std::tie.

#include <utility>

...

inline bool
operator< (Obj const& lhs, Obj const& rhs)
{
  return std::make_pair (lhs.field1, lhs.field2)
       < std::make_pair (rhs.field1, rhs.field2);
}

использование std::pair потребует создания копий элементов, что в некоторых случаях нежелательно.

Это действительно рекомендуемая практика?

См. приведенный ниже вопрос / ответы для получения дополнительной информации, но в целом; подход С++ 11 не вызывает больших накладных расходов и очень прост в реализации.

person Filip Roséen - refp    schedule 03.07.2012
comment
Спасибо. Означает ли это, что первая версия является неправильным порядком ключей в std::map? (в основном это не строгий слабый порядок, который может вызвать ошибки во время выполнения) или это просто другой порядок, чем тот, который я предполагал? - person lezebulon; 03.07.2012
comment
@lezebulon: это не SWO, см. пример в моем ответе. - person Oliver Charlesworth; 03.07.2012
comment
@lezebulon Ваша исходная версия не соответствует требованиям для функции упорядочивания, поэтому ваш код имеет неопределенное поведение. Все может случиться, и на самом деле произойдет много странных вещей. - person James Kanze; 03.07.2012
comment
Канонический способ записи этого условия — return a.field1 < b.field1 || (!(b.field1 < a.field1) && a.field2 < b.field2). Канонический в основном потому, что он использует только <; на практике я не нахожу это ясным и буду использовать его только в контексте шаблона библиотеки (где я не хочу накладывать какие-либо дополнительные ограничения на типы, отличные от тех, которые требуются стандартной библиотекой). - person James Kanze; 03.07.2012
comment
Первое предложение звучит неправильно. Я думаю, вы имели в виду «... следует сравнивать field2 только тогда, когда два значения field1 равны». У вас видимо все наоборот. - person paxdiablo; 07.07.2012
comment
Единственное, о чем вам следует беспокоиться при работе с кортежами, — это строковые поля, потому что это приводит к вызову двух операторов. Используя string::compare, вам нужно вызвать это только один раз, чтобы определить ‹ , ==, › - person steviekm3; 13.10.2019

Подумайте, что произойдет, если a.field1 больше, чем b.field1, но a.field2 меньше, чем b.field2. В этом случае вы сравниваете исключительно на основе field2, а это не то, что вам нужно.

Вы хотите ввести в игру только field2, где поля field1 равны, поэтому вы ищете что-то вроде (псевдокод):

if a.field1 < b.field1: return true
if a.field1 > b.field1: return false
# field1s is equal here.
return a.field2 < b.field2
person paxdiablo    schedule 03.07.2012

Нет. Вам также нужно поймать (a.field1 > b.field1).

Это не строго слабое упорядочение, потому что оно дало бы (1,2) < (2,1), но также и (2,1) < (1,2).

person Oliver Charlesworth    schedule 03.07.2012

Вот версия, основанная на правиле логического короткого замыкания, чтобы избежать явного ветвления.

template<typename T>
bool operator< (T const& a, T const& b)
{
        return (
                 ( a.field1 < b.field1 ) || (( a.field1 == b.field1 ) &&
                 ( a.field2 < b.field2 ))
        );
}

Это предполагает, что ваш примитивный тип field1 имеет operator==. Становится утомительно вводить это для более чем 2 полей, но вы можете использовать std::lexicographical_compare, если ваш класс obj хранит поля внутри std::array<T, N> для некоторого типа T и размера N

template<typename T, int N>
struct obj
{
    std::array<T, N> field;
};

bool operator< (obj const& a, T const& b)
{
        return std::lexicographical_compare(
            a.field.begin(), a.field.end(), 
            b.field.begin(), b.field.end()
        );
}

Обратите внимание, что есть проект документа N3326 в котором обсуждается автоматическое добавление операторов == и < для типов классов.

person TemplateRex    schedule 03.07.2012
comment
Почему вы избегаете явного ветвления? Я не думаю, что это медленнее логических коротких замыканий. - person Mooing Duck; 06.07.2012
comment
@MooingDuck На самом деле не проверял это, и это может даже отличаться для каждой платформы. Просто написал из любопытства. - person TemplateRex; 06.07.2012

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

#define CMP_LT2(a, b) ((a) < (b) ? (a) : (b))
#define CMP_GT2(a, b) ((a) > (b) ? (a) : (b))
#define CMP_LTE2(a, b) ((a) <= (b) ? (a) : (b))
#define CMP_GTE2(a, b) ((a) >= (b) ? (a) : (b))
#define CMP_EQ2(a, b) ((a) == (b))
#define CMP_NEQ2(a, b) ((a) != (b))
#define CMP_LT3(a, b, c) (CMP_EQ2(a, b) ? (c) : CMP_LT2(a, b))
#define CMP_GT3(a, b, c) (CMP_EQ2(a, b) ? (c) : CMP_GT2(a, b))
#define CMP_LTE3(a, b, c) (CMP_EQ2(a, b) ? (c) : CMP_LT2(a, b))
#define CMP_GTE3(a, b, c) (CMP_EQ2(a, b) ? (c) : CMP_GT2(a, b))
#define CMP_EQ3(a, b, c) ((a) == (b) ? (c) : false)
#define CMP_NEQ3(a, b, c) ((a) != (b) ? true : (c))

Затем предположим, что у вас есть:

struct Point3D {
    double x;
    double y;
    double z;
};

И тогда вы пишете:

struct Point3D {
    double x;
    double y;
    double z;

    bool operator<(const Point3D& other) const noexcept
    {
        return CMP_LT3(z, other.z,
               CMP_LT3(y, other.y,
               CMP_LT2(x, other.x)));
    }
};
person ivan.ukr    schedule 17.03.2019

Вы можете использовать вариативные шаблоны в С++ 11 или более поздних версиях.

template<typename T>
bool less_than( const T& a, const T& b )
{
    return a < b;
}

template<typename T, typename... Args>
bool less_than( const T& a, const T& b, Args... args )
(
    if ( a < b )
          return true;
    else if ( b < a )
          return false;
    else
          return less_than(  args...  );
)

Тогда вы бы назвали как

return less_than(a.x,b.x,
                 a.y,b.y,
                 a.z,b.z);

Он поддерживает любое количество полей или типов, если тип имеет оператор ‹. Вы можете смешивать типы.

person steviekm3    schedule 11.10.2019
comment
Это не дает действительного строгого слабого порядка. Кроме того, на самом деле нет причин реализовывать это самостоятельно, когда вы можете просто использовать std::tie(a.x,a.y,a.z) < std::tie(b.x,b.y,b.z). - person interjay; 12.10.2019
comment
Можете ли вы привести пример того, почему не выполняется строгое слабое упорядочение? - person steviekm3; 13.10.2019
comment
Ну, вы отредактировали его после моего комментария, так что теперь все в порядке. Предыдущая версия была функционально идентична a.x < b.x || a.y < b.y || ..., поэтому она не работала, например. для a={2,1,..}, b={1,2,...}. - person interjay; 13.10.2019
comment
Хорошо, понял. Во-первых, на практике я использую не это, а слегка модифицированное, которое использует общее сравнение, которое возвращает -1,0 или 1, чтобы избежать необходимости выполнять два вызова оператора для строковых полей. Я думаю, что метод tuple/tie проще/лучше, если он также может использовать преимущества сравнения строк, поэтому строки не нужно повторять дважды. - person steviekm3; 13.10.2019