Подтвердить что ты не робот

Должен ли я использовать memcmp или прикованные равные операции, когда оба дают одинаковый результат?

Предпосылка. Рассмотрим такой класс или структуру T, что для двух объектов a и b типа T

memcmp(&a, &b, sizeof(T)) == 0

дает тот же результат, что и

a.member1 == b.member1 && a.member2 == b.member2 && ...

(memberN является нестатической членной переменной T).

Вопрос. Когда следует использовать memcmp для сравнения a и b для равенства, а когда нужно использовать цепочку ==?


Вот простой пример:

struct vector
{
    int x, y;
};

Чтобы перегрузить оператор == для vector, есть две возможности (если они гарантированно дают тот же результат):

bool operator==(vector lhs, vector rhs)
{ return lhs.x == rhs.x && lhs.y == rhs.y; }

или

bool operator==(vector lhs, vector rhs)
{ return memcmp(&lhs, &rhs, sizeof(vector)) == 0; }

Теперь, если новый элемент должен быть добавлен в vector, например, компонент z:

  • Если для реализации operator== использовался ==, его нужно было бы изменить.
  • Если вместо этого использовался memcmp, operator== не нужно было бы изменять вообще.

Но я думаю, что использование прикованного == передает более четкое значение. Хотя для большого T со многими членами memcmp более заманчиво. Кроме того, есть ли улучшение производительности при использовании memcmp over == s? Что еще нужно учитывать?

4b9b3361

Ответ 1

Что касается предпосылки memcmp, дающей тот же результат, что и членские сравнения с ==, хотя это предположение часто выполняется на практике, оно несколько хрупкое.

Изменение компиляторов или параметров компилятора может теоретически нарушить это предварительное условие. Более того, обслуживание кода (и 80% всей работы по программированию - это обслуживание, IIRC) может сломать его, добавив или удалив участников, сделав класс полиморфным, добавив пользовательские перегрузки == и т.д. И, как упоминалось в одном из комментариев, предварительное условие может сохраняться для статических переменных, в то время как оно не выполняется для автоматических переменных, а затем работа по обслуживанию, которая создает нестатические объекты, может делать Bad Things & trade;.

И в связи с вопросом о том, следует ли использовать memcmp или member-wise == для реализации оператора == для класса, во-первых, это ложная дихотомия, поскольку это не единственные варианты.

Например, при использовании функции compare может быть меньше работы и более ремонтопригодной для использования автоматической генерации перегрузок операторских операторов. Функция std::string::compare является примером такой функции.

Во-вторых, ответ на выбор реализации зависит от того, что вы считаете важным, например:

  • следует стремиться максимизировать эффективность выполнения или

  • следует стремиться создать яркий код или

  • нужно искать самый короткий, самый быстрый для записи код или

  • следует стремиться к тому, чтобы использовать класс безопасный, или

  • что-то еще, возможно?

Создание реляционных операторов.

Возможно, вы слышали о CRTP, Curiously Recurring Template Pattern. Насколько я помню, оно было изобретено для решения задачи генерации реляционных перегрузок операторов. Возможно, я могу смириться с чем-то другим, но в любом случае:

template< class Derived >
struct Relops_from_compare
{
    friend
    auto operator!=( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) != 0; }

    friend
    auto operator<( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) < 0; }

    friend
    auto operator<=( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) <= 0; }

    friend
    auto operator==( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) == 0; }

    friend
    auto operator>=( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) >= 0; }

    friend
    auto operator>( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) > 0; }
};

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

Реализация A: сравнение вычитанием.

Это класс, предоставляющий полный набор реляционных операторов без использования memcmp или ==:

struct Vector
    : Relops_from_compare< Vector >
{
    int x, y, z;

    // This implementation assumes no overflow occurs.
    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        if( const auto r = a.x - b.x ) { return r; }
        if( const auto r = a.y - b.y ) { return r; }
        return a.z - b.z;
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

Реализация B: сравнение через memcmp.

Это тот же класс, реализованный с использованием memcmp; Я думаю, вы согласитесь, что этот код масштабируется лучше и проще:

struct Vector
    : Relops_from_compare< Vector >
{
    int x, y, z;

    // This implementation requires that there is no padding.
    // Also, it doesn't deal with negative numbers for < or >.
    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        static_assert( sizeof( Vector ) == 3*sizeof( x ), "!" );
        return memcmp( &a, &b, sizeof( Vector ) );
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

Реализация C: элемент сравнения по члену.

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

struct Vector
    : Relops_from_compare< Vector >
{
    int x, y, z;

    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        if( a.x < b.x ) { return -1; }
        if( a.x > b.x ) { return +1; }
        if( a.y < b.y ) { return -1; }
        if( a.y > b.y ) { return +1; }
        if( a.z < b.z ) { return -1; }
        if( a.z > b.z ) { return +1; }
        return 0;
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

Реализация D: compare в терминах реляционных операторов.

Это тип реализации, изменяющий естественный порядок вещей путем реализации compare в терминах < и ==, которые предоставляются непосредственно и реализованы в терминах std::tuple сравнений (используя std::tie).

struct Vector
{
    int x, y, z;

    friend
    auto operator<( const Vector& a, const Vector& b )
        -> bool
    {
        using std::tie;
        return tie( a.x, a.y, a.z ) < tie( b.x, b.y, b.z );
    }

    friend
    auto operator==( const Vector& a, const Vector& b )
        -> bool
    {
        using std::tie;
        return tie( a.x, a.y, a.z ) == tie( b.x, b.y, b.z );
    }

    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        return (a < b? -1 : a == b? 0 : +1);
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

Как указано, код клиента, например, > требуется a using namespace std::rel_ops;.

Альтернативы включают добавление всех других операторов к указанному выше (намного больше кода) или использование схемы генерации CRTP-оператора, которая реализует другие операторы в терминах < и = (возможно, неэффективно).

Реализация E: сравнение путем ручного использования < и ==.

Эта реализация является результатом, не применяющим абстракции, просто ударяясь о клавиатуру и непосредственно записывая, что должна делать машина:

struct Vector
{
    int x, y, z;

    friend
    auto operator<( const Vector& a, const Vector& b )
        -> bool
    {
        return (
            a.x < b.x ||
            a.x == b.x && (
                a.y < b.y ||
                a.y == b.y && (
                    a.z < b.z
                    )
                )
            );
    }

    friend
    auto operator==( const Vector& a, const Vector& b )
        -> bool
    {
        return
            a.x == b.x &&
            a.y == b.y &&
            a.z == b.z;
    }

    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        return (a < b? -1 : a == b? 0 : +1);
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

Что выбрать.

Учитывая список возможных аспектов для оценки большинства, таких как безопасность, ясность, эффективность, краткость, оцените каждый подход выше.

Затем выберите ту, которая вам лучше всего подходит, или один из подходов, которые кажутся примерно одинаковыми.

Руководство. Для безопасности вы не захотите выбирать подход A, вычитание, поскольку оно опирается на предположение о значениях. Обратите внимание, что также опция B, memcmp, небезопасна как реализация для общего случая, но может преуспеть для всего лишь == и !=. Для эффективности вы должны лучше MEASURE, с соответствующими параметрами компилятора и средой, а также помните поговорку Дональда Кнута: "преждевременная оптимизация - это корень всего зла" (т.е. Тратить время на то, что может быть контрпродуктивным).

Ответ 2

Если, как вы говорите, вы выбрали такие типы, чтобы два решения дали одинаковые результаты (предположительно, тогда у вас нет косвенных данных, а выравнивание/заполнение все равно), то, очевидно, вы можете использовать то, что решение, которое вам нравится.

Что нужно учитывать:

  • Производительность: Я сомневаюсь, что вы увидите много, если есть какая-либо разница, но измерьте ее, если хотите,
  • Безопасность: Хорошо, вы говорите, что два решения одинаковы для вашего T, но так ли? Действительно ли они? На всех системах? Подходит ли ваш подход memcmp к переносу? Вероятно, нет;
  • Ясность: Если ваши предварительные условия когда-либо меняются, и вы недостаточно адекватно комментировали свое использование memcmp, то ваша программа подвержена разрыву — поэтому вы сделали его хрупким;
  • Консистенция: Предположительно, вы используете == в другом месте; конечно, вам придется делать это для каждого T, который не соответствует вашим предпосылкам; если это не преднамеренная оптимизация специализации для T, вы можете подумать о том, чтобы придерживаться единого подхода во всей вашей программе;
  • Простота использования: Конечно, довольно легко пропустить участника из прикованного ==, особенно если ваш список членов когда-либо растет.

Ответ 3

Если два решения верны, предпочитайте более читаемый. Я бы сказал, что для программиста на С++ == более читаем, чем memcmp. Я бы зашел так далеко, чтобы использовать std::tie вместо цепочки:

bool operator==(const vector &lhs, const vector &rhs)
{ return std::tie(lhs.x, lhs.y) == std::tie(rhs.x, rhs.y); }

Ответ 4

Если какая-либо только в том случае, если структура POD и безопасно memcmp сопоставима (даже не все числовые типы...), результат остается тем же, и вопрос о читаемости и производительности.

Читаемость? Я думаю, что это скорее основанный на мнениях вопрос, но я бы предпочел operator==.

Производительность? operator== - оператор короткого замыкания. Здесь у вас больше контроля над вашей программой, потому что вы можете изменить порядок сравнения.

Хотя a == b && c == d и c == d && a == b эквивалентны в терминах алгоритмической логики (результат один и тот же), они не эквивалентны с точки зрения произведенной сборки, "фоновой логики" и, возможно, производительности.

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

В примере:

  • Если оба оператора примерно одинаково вероятны, чтобы дать false, вы захотите, чтобы более дешевое выражение сначала пропустило более сложное сравнение, если это возможно.
  • Если оба утверждения грубо одинаково сложны, и вы заранее знаете, что c == d более вероятно, будет ложным, чем a == b, сначала следует сравнить c и d.

Можно скорректировать последовательность сравнения в зависимости от проблемы, используя operator==, тогда как memcmp не дает вам такой свободы.

PS: Вы бы хотели его измерить, но для небольшой структуры с тремя членами MS VS 2013 создает немного более сложную сборку для случая memcmp. Я ожидал бы, что решение operator== будет иметь более высокую производительность (если бы воздействие было измеримым) в этом случае.

-/Edith -

Примечание. Даже элементы структуры POD могут перегружать operator==.

Рассмотрим:

#include <iostream>
#include <iomanip>

struct A { int * p; };

bool operator== (A const &a, A const &b) { return *(a.p) == *(b.p); }

struct B { A m; };

bool operator== (B const &a, B const &b) { return a.m == b.m; }

int main()
{
  int a(1), b(1);
  B x, y;
  x.m.p = &a;
  y.m.p = &b;
  std::cout << std::boolalpha;
  std::cout << (memcmp(&x, &y, sizeof(B)) == 0) << "\n";
  std::cout << (x == y) << "\n";
  return 0;
}

Печать

false
true

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

Ответ 5

Вы наложили очень сильное условие, что нет отступов (я не предполагаю ни между членами класса, ни внутри этих членов). Я предполагаю, что вы также намеревались исключить любые "скрытые" данные домашнего хозяйства из класса. Кроме того, сам вопрос подразумевает, что мы всегда сравниваем объекты точно такого же типа. В таких сильных условиях, вероятно, нет способа найти контрпример, который бы сравнил сравнение memcmp с сравнением ==.

Стоит ли использовать memcmp по соображениям производительности... ну, если у вас действительно есть веская причина для агрессивной оптимизации некоторых критических фрагментов кода и профилирования, это улучшение после перехода с == на memcmp, то определенно идти вперед. Но я бы не использовал его как обычную технику для написания операторов сравнения, даже если ваш класс удовлетворяет требованиям.

Ответ 6

== лучше, потому что memcmp сравнивает данные чистой памяти (сравнение этого способа может быть неправильным во многих ситуациях, таких как std::string, массивные имитирующие классы или типы, которые могут быть равны, даже если они не являются совершенно идентичны). Поскольку внутри ваших классов могут быть такие типы, вы должны всегда использовать свои собственные операторы вместо сравнения необработанных данных памяти.

== также лучше, потому что он более читабельен, чем какая-то странная функция.