运算符==对其操作数进行变异是不是一种糟糕的做法
Is it bad practice for operator== to mutate its operands?
场景
我有一个班,我希望能够比较平等。这个类很大(它包含一个位图图像(,我会多次比较它,所以为了提高效率,我对数据进行哈希处理,只有在哈希匹配的情况下才进行完全相等的检查。此外,我将只比较对象的一小部分,所以我只在第一次进行相等性检查时计算哈希,然后将存储的值用于后续调用。
示例
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==
函数是否应该被视为与其众所周知的语义相反?如果以上任何一个的答案都是"是",那么什么是更合适的方法呢?
对于mutable
成员来说,这似乎是一个非常有效的用例。您仍然可以(也应该(让operator==
通过const引用获取参数,并为该类指定一个mutable
成员作为哈希值。
然后,您的类将有一个用于散列值的getter,该散列值本身被标记为const
方法,并且该lazy在第一次调用时计算散列值。这实际上是一个很好的例子,说明了为什么mutable
被添加到该语言中,因为从用户的角度来看,它不会更改对象,它只是用于在内部缓存昂贵操作的值的实现细节。
对要缓存但不影响公共接口的数据使用mutable
。
U现在,";突变"→mutable
。
然后从逻辑const
-ness的角度来思考,是什么保证了对象为正在使用的代码提供。
您不应该在比较时修改对象。但是,此函数不会在逻辑上修改对象。简单的解决方案:使hash
可变,因为计算哈希是一种兑现形式。请参阅:是否执行';可变';关键字除了允许常量函数修改变量之外还有其他用途吗?
- 不建议在比较函数或运算符中产生副作用。如果您能够设法将散列计算作为类初始化的一部分,则会更好。另一种选择是有一个管理器类来负责这一点。注意:即使看似无害的突变也需要在多线程应用程序中进行锁定
- 此外,我还建议避免在数据结构并非绝对琐碎的类中使用相等运算符。通常情况下,项目的进度会产生对比较策略(参数(的需求,并且相等运算符的接口变得不足。在这种情况下,添加比较方法或函子将不需要反映参数不变性的标准运算符==接口
- 如果1。和2。对于您的情况,您可以使用c++关键字mutable作为hash值成员。这将允许您修改它,即使是从const类方法或const声明的变量
是的,引入语义上出乎意料的副作用总是一个坏主意。除了提到的其他原因外:永远假设你写的任何代码都将永远只被其他甚至没有听说过你名字的人使用,然后从这个角度考虑你的设计选择。
当使用你的代码库的人发现他的应用程序很慢,并试图优化它时,如果它在==重载中,他会浪费很长时间来寻找性能泄漏,因为从语义的角度来看,他不希望它做的不仅仅是简单的对象比较。将可能代价高昂的操作隐藏在语义廉价的操作中是一种糟糕的代码混淆形式。
您可以使用可变路由,但我不确定是否需要。您可以在需要时执行本地缓存,而不必使用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作为一个成员变量,而不是将其本地化到需要的地方吗?请注意,如果我们真的想的话,我们可以进一步摆脱"静态"使用,我们所要做的就是制作一个专用的结构/类
- 有符号的int和int-有没有一种方法可以在C++中区分它们
- 有一个打印语句的函数是一种糟糕的编程实践吗
- 有没有一种方法可以创建一个带有哈希表的数据库,该哈希表具有恒定时间查找功能
- 有没有一种方法可以在编译时获得作用域类名
- 对于C++中使用智能指针的指针算术限制,有没有一种变通方法
- 一种在C++中读取TXT配置文件的简单方法
- 有没有一种方法可以测量c++程序的运行时内存使用情况
- 有没有一种方法可以使用placement new将堆叠对象分配给分配的内存
- 在调用接收数组的方法时,模板化数组大小是不是一种糟糕的做法
- 有没有一种方法可以通过"typedef"为重新定义的基本类型定义特征和强制转换运算符
- 有没有一种"cleaner"的方法可以在指向基的指针向量中找到派生类的第一个实例?
- 有没有一种代码密度较低的方法来使用非默认构造函数初始化数组?
- 将错误返回给调用方而不是立即在 C++ 中抛出错误是否是一种好的做法
- 在 c++ 中,有一种方法可以创建一个包含地图作为值的树状地图?
- 有没有一种优雅而快速的方法来测试整数中的 1 位是否位于连续区域
- 在运行时检查继承是否只有一种类型和 void*
- C++ STD 函数运算符:有没有一种方法可以通过函数将一个向量映射到另一个向量上?
- 找到一种有效的方法,在 2 个巨大的缓冲区上执行 MAX,每字节字节
- 将右值引用作为左操作数重载加法操作符被认为是一种良好的做法
- 运算符==对其操作数进行变异是不是一种糟糕的做法