脚本
我有一个课我希望能够比较平等。这个类很大(它包含一个位图图像),我会多次比较它,所以为了提高效率,我要对数据进行哈希处理,只检查哈希是否匹配。此外,我将只比较我的对象的一小部分,所以我只是在第一次完成相等性检查时计算哈希值,然后使用存储的值进行后续调用。
例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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);
此外,给出了以下关于运算符重载的一般建议:
Always stick to the operator’s well-known semantics.
问题
我的函数必须能够改变它的操作数才能执行散列,所以我必须使它们非const。是否有任何潜在的负面后果(示例可能是标准库函数或STL容器,期望operator==具有const操作数)?
如果变异operator==函数被认为与其众所周知的语义相反,如果该变异没有任何可观察到的影响(因为用户无法看到散列的内容)?
如果上述任何一个的答案都是"是",那么什么是更合适的方法呢?
-
我怀疑存在堆栈溢出问题的所有X的99%是"X是一种不好的做法吗?"是不好的做法。提问的人已经在他们的代码中做了X,并且正在寻找道德支持,只是把它留在那里。另外1%的人提出了一些很棒的新练习,并且只是炫耀。
-
为什么在类声明中有friend函数?为什么在类声明中定义的函数使用显式inline? ==运算符可以只是一个成员。左侧是隐式的(this对象)。 bool operator == (const Foo &rhs) { ... }。
-
如果getHash函数实际上是在成员对象或基类上怎么办?假设您派生自某个具有getHash功能的widget类。你是否关心getHash执行惰性实例化,只要它有效,它是破坏性的吗?
-
这个问题可以解释为什么谷歌c ++风格指南不鼓励操作员重载。 google-styleguide.googlecode.com/svn/trunk/…
-
@Kaz如果将==声明为成员,则会强制其中一个操作数为this指针(可能并不总是您想要的)。我读过的大多数指南都建议将它(以及其他比较运算符)作为非成员朋友函数实现。
-
@Kaz我在类声明中使用'inline'和'friend'可能是一种误解,这就是我在一个我发现的例子中看到它的实现方式。我相信类声明中的friend函数实际上是非成员。例如,见这个。我认为我添加inline是因为错误地认为只有成员函数隐含了它,但我现在看到我可能错了,并且它适用于类声明中的任何内容。
-
如果左操作数是Foo,为什么使用非成员函数是有利的,如果该函数必须是该类的friend才能获得访问权限?非成员函数的优点是它们使用类公共接口(不是朋友)。您可以在不更改类声明的情况下编写它们(例如,对于第三方类库)。只有类Foo的朋友并且操作在类型Foo的左操作数上的朋友通过引用传入的函数尖叫"我是伪装的成员函数"。
-
@Kaz如果左操作数不是Foo怎么办?
-
如果左操作数不是Foo,则C ++表示它不能是类Foo的(非静态)成员函数。如果函数需要访问某些类的Foo位,那么它可以是Foo类的静态成员。如果它需要访问不同类的几个私有部分,那么它可以是所有这些类的非成员朋友。如果它在任何类中都不需要特殊访问,那么它可以只是一个普通的非成员函数,没有人声明为朋友。
-
@Kaz如果它被定义为静态成员,你会如何调用operator==?它甚至可能吗?我无法在MSVC中编译它(即使我完全符合条件),这个问题也没有说明。
-
啊,不。运算符不能是静态成员函数。如果这样的东西是可以定义的,那么从类中调用就很容易了,但是从外面它看起来像Foo::operator == (a, b)。
-
@Kaz是的,这是我在编译器抱怨'Foo::operator ==' must be a non-static member时尝试调用Foo::operator==(a, b)(这在任何情况下都是令人不快的语法)时得出的结论。 这个答案声称它可以作为一个static friend函数,但是当我尝试这个时,我的编译器会抱怨'==' : is not a member of 'Foo,所以我不确定那个答案是在说什么。
-
我对这个答案添加了评论。
-
static friend不再是成员函数,这就是它的工作原理。 static具有非成员函数的含义。
-
@Kaz啊是的,所以确实如此,我忘了恢复(a == b)而不是Foo::operator==(a, b)版本的static friend。
对于mutable成员来说,它似乎是一个非常有效的用例。您可以(并且应该)仍然使operator==通过const引用获取参数,并为类提供哈希值的mutable成员。
然后,您的类将具有哈希值的getter,该哈希值本身被标记为const方法,并且在第一次调用时延迟评估哈希值。这实际上是为什么mutable被添加到语言中的一个很好的例子,因为它没有从用户的角度改变对象,它只是一个实现细节,用于在内部缓存昂贵的操作的价值。
-
因为没有提到 - 如果你打算使用mutable来执行缓存,建议以线程安全的方式这样做,因为否则operator==修改Foo的事实可能通过同时从两个线程调用同一个对象来观察它。即使您从未在代码中明确地执行此操作,至少有一些C ++委员会成员认为标准库执行此操作是合法的。
-
@Mankarse感谢您的评论和链接。
-
请注意,通过使用0作为sentinal哈希并执行if(atomic_load(&hash)!=0)atomic_store(&hash,calc_hash());,可以使其线程安全
使用mutable表示要缓存但不影响公共接口的数据。
你现在,'变异” → mutable。
然后根据逻辑const -ness进行思考,保证对象提供给使用代码。
你永远不应该在比较时修改对象。但是,此函数不会在逻辑上修改对象。简单的解决方案:make hash可变,因为计算哈希是一种兑现形式。看到:
除了允许变量被const函数修改之外,'mutable'关键字是否有任何其他用途?
是的,引入语义上意想不到的副作用总是一个坏主意。除了提到的其他原因:总是假设您编写的任何代码将永远只有其他人甚至没有听说过您的名字,然后从这个角度考虑您的设计选择。
当有人使用你的代码库发现他的应用程序很慢,并试图优化它时,如果它在==重载内,他将浪费很多时间试图找到性能泄漏,因为他不期望它,从语义点对于视图来说,要做的不仅仅是简单的对象比较。在语义上廉价的操作中隐藏可能代价高昂的操作是一种糟糕的代码混淆形式。
-
有趣的是,与其他一条评论中链接的Google风格指南中提到的相同。散列是为了在大多数情况下降低==操作的成本而设计的,但你是正确的,因为在某些情况下会出现这种情况,例如:用户从不多次比较对象。所以你建议我更喜欢命名的相等成员函数(例如bool equals(Foo& rhs)),并记录那里的性能语义?
-
是的,这会更清晰,让==运算符只是做普通的,可能很昂贵的简单比较。如果编码器放入4GB二进制文件,编码器将知道该操作在计算上是昂贵的。
-
咦?所以首先你要说==应该是计算上便宜的,因为它在语义上很便宜,现在你说它不应该被优化。 - 我认为你的意思是,==的计算成本应该是可预测的,但只要你有一个固定和合理的最坏情况界限,我就不会发现性能变化有什么问题。根据难以预测的情况,有许多操作可能需要不同的时间,例如std::vector::push_back非常快,但有时需要重新定位整个阵列。
-
是的,措辞并不像嘿嘿一样干净。我的意思是比较的计算成本应该在最坏的情况下与其操作数的大小具有可预测的线性关系。引入副作用可使其成为指数。
-
它为什么会变成指数?
-
如果您在比较之前对操作数进行哈希处理,或者引入其他副作用,例如(向下)按需加载或任何其他副作用。
-
只有在非常糟糕的哈希或下载实现中,才会比线性时间更糟糕,但无论如何。
-
"可能"是我评论中的关键词,任何"可能"导致性能问题的内容都不应该隐藏在不会触及可能性的语法背后。
不建议在比较函数或运算符中具有副作用。如果您可以设法计算哈希作为类初始化的一部分,那将会更好。另一个选择是拥有一个负责这个的经理类。注意:即使是看似无辜的突变也需要在多线程应用程序中锁定。
另外,我建议避免对数据结构不是绝对平凡的类使用相等运算符。通常,项目的进度需要比较策略(参数),并且相等运算符的接口变得不足。在这种情况下,添加compare方法或functor将不需要反映标准operator ==接口的参数不变性。
如果1.和2.对于您的情况看起来有点过分,您可以使用c ++关键字mutable作为哈希值成员。这将允许您甚至从const类方法或const声明的变量修改它
你可以走可变路线,但我不确定是否需要。您可以在需要时执行本地缓存,而无需使用mutable。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #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;
} |
输出:
1 2 3
| in hash calculate
1
1 |
你能否给我一个很好的理由来保持为什么保持isHashed作为成员变量是必要的,而不是让它在需要的地方?请注意,如果我们真的想要的话,我们可以进一步摆脱"静态"使用,我们所有的东西都是做一个专门的结构/类
-
你的意思是在每个Foo旁边保持一个HashCompare,然后每次都在本地检查或者有一个免费的功能来做到这一点?这破坏了封装(isHashed是Foo的属性,为什么它存在于Foo之外?)并且意味着我必须在需要的地方将它们作为对传递。有什么好处?请澄清我是否误解了您的建议。
-
@JBentley用一个完整的例子重新编辑