Является ли неправильной практикой для оператора == мутировать свои операнды?
Сценарий
У меня есть класс, который я хочу сравнить с равенством. Класс большой (он содержит растровое изображение), и я буду сравнивать его несколько раз, поэтому для эффективности я собираю данные и выполняю полную проверку равенства, если хэши совпадают. Кроме того, я буду сравнивать только небольшое подмножество своих объектов, поэтому я только вычисляю хэш при первой проверке равенства, затем используя сохраненное значение для последующих вызовов.
Пример
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==
считается противоречащей ее хорошо известной семантике, если мутация не имеет каких-либо наблюдаемых эффектов (потому что у пользователя нет возможности видеть содержимое хэша)?
-
Если ответ на любой из вышеперечисленных вопросов - "да", то какой будет более подходящий подход?
Ответы
Ответ 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 в качестве переменной-члена необходимо вместо того, чтобы сделать его локальным, где это необходимо? Обратите внимание, что мы можем еще больше уйти от "статического" использования, если мы действительно хотим, все, что у нас есть, - это сделать выделенную структуру/класс