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

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

Сценарий

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

Пример

class Foo
{
public:

   Foo(int data) : fooData(data), notHashed(true) {}

private:

   void calculateHash()
   {
      hash = 0; // Replace with hashing algorithm
      notHashed = false;
   }

   int getHash()
   {
      if (notHashed) calculateHash();
      return hash;
   }

   inline friend bool operator==(Foo& lhs, Foo& rhs)
   {
      if (lhs.getHash() == rhs.getHash())
      {
         return (lhs.fooData == rhs.fooData);
      }
      else return false;
   }

   int fooData;
   int hash;
   bool notHashed;
};

Фон

В соответствии с руководством по этому ответу каноническая форма оператора равенства:

inline bool operator==(const X& lhs, const X& rhs);

Кроме того, приведен следующий общий совет для перегрузки оператора :

Всегда придерживайтесь операторов хорошо известной семантики.

Вопросы

  • Моя функция должна иметь возможность мутировать его операнды, чтобы выполнить хеширование, поэтому мне пришлось сделать их не const. Существуют ли какие-либо потенциальные негативные последствия этого (примерами могут быть стандартные библиотечные функции или контейнеры STL, которые ожидают, что operator== имеет операнды const)?

  • Если мутирующая функция operator== считается противоречащей ее хорошо известной семантике, если мутация не имеет каких-либо наблюдаемых эффектов (потому что у пользователя нет возможности видеть содержимое хэша)?

  • Если ответ на любой из вышеперечисленных вопросов - "да", то какой будет более подходящий подход?

4b9b3361

Ответ 1

Похоже, что это действительно действующая утилита для члена mutable. Вы можете (и должны) по-прежнему сделать operator== значение параметра по ссылке const и дать классу a mutable член для хеш-значения.

Тогда ваш класс будет иметь getter для хеш-значения, которое само помечается как метод const и что lazy-оценивает значение хэша при вызове в первый раз. Это на самом деле хороший пример того, почему mutable был добавлен в язык, поскольку он не изменяет объект с точки зрения пользователя, это всего лишь деталь реализации для кэширования ценности дорогостоящей операции внутри.

Ответ 2

Используйте mutable для данных, которые вы хотите кэшировать, но которые не влияют на общий интерфейс.

U теперь, "mutate" → mutable.

Затем подумайте с точки зрения логического const -ness, что гарантирует, что объект предлагает код использования.

Ответ 3

Вы не должны изменять объект при сравнении. Однако эта функция не логически модифицирует объект. Простое решение: make hash mutable, поскольку вычисление хэша является формой обналичивания. Видеть: Имеет ли ключевое слово "изменчивое" какие-либо цели, кроме возможности изменения переменной с помощью функции const?

Ответ 4

  • Не рекомендуется побочный эффект в функции сравнения или оператора. Будет лучше, если вы сможете вычислить хэш как часть инициализации класса. Другой вариант заключается в том, что за это отвечает класс менеджера. Обратите внимание: что даже то, что кажется невинной мутацией, потребует блокировки в многопоточном приложении.
  • Также я рекомендую избегать использования оператора равенства для классов, где структура данных не является абсолютно тривиальной. Очень часто прогресс проекта создает необходимость в сравнительной политике (аргументах), а интерфейс оператора равенства становится недостаточным. В этом случае добавление метода сравнения или функтора не обязательно должно отражать стандартный интерфейс operator == для неизменности аргументов.
  • Если 1. и 2. кажутся излишними для вашего случая, вы можете использовать ключевое слово С++, изменяемое для члена хеш-значения. Это позволит вам изменить его даже из метода const класса или объявленной переменной const

Ответ 5

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

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

Ответ 6

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

#include <iostream>
#include <functional> //for hash

using namespace std;

template<typename ReturnType>
class HashCompare{
public:
    ReturnType getHash()const{
        static bool isHashed = false;
        static ReturnType cachedHashValue = ReturnType();
        if(!isHashed){
            isHashed = true;
            cachedHashValue = calculate();
        }
        return cachedHashValue;
    }
protected:
    //derived class should implement this but use this.getHash()
    virtual ReturnType calculate()const = 0;
};



class ReadOnlyString: public HashCompare<size_t>{
private:
    const std::string& s;
public:
    ReadOnlyString(const char * s):s(s){};
    ReadOnlyString(const std::string& s): s(s){}

    bool equals(const ReadOnlyString& str)const{
        return getHash() == str.getHash();
    }
protected:
    size_t calculate()const{
        std::cout << "in hash calculate " << endl;
        std::hash<std::string> str_hash;
        return str_hash(this->s);
    }
};

bool operator==(const ReadOnlyString& lhs, const ReadOnlyString& rhs){ return lhs.equals(rhs); }


int main(){
    ReadOnlyString str = "test";
    ReadOnlyString str2 = "TEST";
    cout << (str == str2) << endl;
    cout << (str == str2) << endl;
}

Вывод:

 in hash calculate 
 1
 1

Можете ли вы дать мне вескую причину, чтобы сохранить, почему сохранение isHashed в качестве переменной-члена необходимо вместо того, чтобы сделать его локальным, где это необходимо? Обратите внимание, что мы можем еще больше уйти от "статического" использования, если мы действительно хотим, все, что у нас есть, - это сделать выделенную структуру/класс