Эффективные и простые операторы сравнения для структур

Приложение, в котором я работаю, в настоящее время имеет большое количество структур, которые содержат данные, которые поступают из различных источников, таких как базы данных и файлы. Например, например:

struct A 
{
    float val1;
    std::string val2;
    int val3;

    bool operator < (const A& other) const;
};

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

bool A:operator < (const A& o) const {
    return val1 < o.val1 || 
        (val1 == o.val1 && ( val2 < o.val2 || 
            (val2 == o.val2 && ( val3 < o.val3 ) ) );
}

Это кажется эффективным, но имеет несколько недостатков:

  1. Эти выражения становятся огромными, если структуры составляют десяток или более членов.
  2. Грубо писать и поддерживать, если члены меняются.
  3. Это необходимо для каждой структуры отдельно.

Есть ли более удобный способ сравнения таких структур?

Ответ 1

Вы можете использовать встроенное сравнение, которое поставляется с <tuple> следующим образом:

#include <tuple>

bool A::operator < (const A& rhs) const {
    return std::tie(val1, val2, val3) < std::tie(rhs.val1, rhs.val2, rhs.val3);
}

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

Позвольте мне добавить еще три комментария к operator <.

  1. Когда у вас есть operator <, клиенты будут ожидать, что все другие операторы сравнения также будут предоставлены. Прежде чем иметь трехстороннее сравнение в С++ 20, вы можете избежать ненужного кода шаблона, например, используя библиотеку операторов Boost:

    #include <boost/operators.hpp>
    
    struct A : private boost::totally_ordered<A> { /* ... */ };
    

    который генерирует все операторы, основанные на operator < и operator == для вас.

  2. В вашем примере нет необходимости, чтобы оператор был членом A Вы можете сделать его свободной функцией, что предпочтительнее (см. Здесь для обоснования).

  3. Если нет никакого внутреннего порядка, связанного с A и вам просто нужен operator < для хранения экземпляров в виде ключей на std::map, подумайте о предоставлении именованного предиката.

Ответ 2

Отличный ответ lubgr.

Еще одно уточнение, которое я выполняю, - это создание функции-члена as_tuple для любого объекта, который должен быть упорядочен его членами:

#include <string>
#include <tuple>
#include <iostream>

struct A 
{
    float val1;
    std::string val2;
    int val3;

    // provide easy conversion to tuple
    auto as_tuple() const
    {
        return std::tie(val1, val2, val3);
    }
};

Который часто вызывает мысли об общей системе создания объектов и кортежей, взаимозаменяемых с точки зрения сравнений

template<class T> auto as_tuple(T&& l) -> decltype(l.as_tuple()) 
{
    return l.as_tuple();
}

template<class...Ts> 
auto as_tuple(std::tuple<Ts...> const& tup) 
-> decltype(auto)
{
    return tup;
}

template<class L, class R>
auto operator < (L const& l, R const& r)
-> decltype(as_tuple(l), void(), as_tuple(r), void(), bool())
{
    return as_tuple(l) < as_tuple(r);
}

Что позволяет такой код, как:

int main()
{
    auto a = A { 1.1, "foo", 0 };
    auto b = A { 1.1, "foo", 1 };

    auto test1 = a < b;
    std::cout << test1 << std::endl;

    auto test2 = a < std::make_tuple(1.1, "bar", 0);
    std::cout << test2 << std::endl;

    auto test3 = std::make_tuple(1.0, "bar", 0) < std::make_tuple(1.1, "bar", 0);
    std::cout << test3 << std::endl;

    auto test4 = a < std::make_tuple(2l, std::string("bar"), 0);
    std::cout << test4 << std::endl;

}

пример: http://coliru.stacked-crooked.com/a/ead750f3f65e3ee9