21

设想

我有一门课,我希望能够比较它的平等性。该类很大(它包含一个位图图像),我将对其进行多次比较,因此为了提高效率,我对数据进行哈希处理,并且仅在哈希匹配时进行完全相等检查。此外,我只会比较我的对象的一小部分,所以我只在第一次完成相等性检查时计算哈希值,然后将存储的值用于后续调用。

例子

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);

此外,给出了以下关于运算符重载的一般建议

始终坚持运营商众所周知的语义。

问题

  1. 我的函数必须能够改变它的操作数才能执行散列,所以我不得不将它们设为非const. 这是否有任何潜在的负面后果(示例可能是标准库函数或期望operator==const操作数的 STL 容器)?

  2. operator==如果突变没有任何可观察到的影响(因为用户无法看到哈希的内容),是否应该认为突变函数与其众所周知的语义相反?

  3. 如果上述任何一个的答案是“是”,那么更合适的方法是什么?

4

6 回答 6

37

对于成员来说,这似乎是一个完全有效的用例mutable。您仍然可以(并且应该)operator==通过 const 引用获取参数,并为该类提供mutable哈希值的成员。

然后,您的类将有一个散列值的获取器,该散列值本身被标记为const方法,并且在第一次调用时会延迟评估散列值。这实际上是一个很好的例子,说明为什么mutable要添加到语言中,因为它不会从用户的角度改变对象,它只是在内部缓存昂贵操作的值的实现细节。

于 2013-04-12T15:15:20.957 回答
12

用于mutable要缓存但不影响公共接口的数据。

你现在,“变异”→ mutable

然后从逻辑性 const的角度考虑,对象为使用代码提供了什么保证。

于 2013-04-12T15:14:21.330 回答
3

您永远不应该在比较时修改对象。但是,此函数不会在逻辑上修改对象。简单的解决方案:使hash可变,因为计算哈希是兑现的一种形式。请参阅: 'mutable' 关键字除了允许变量由 const 函数修改之外还有其他用途吗?

于 2013-04-12T15:18:21.007 回答
1
  1. 不建议在比较函数或运算符中产生副作用。如果您可以设法计算哈希作为类初始化的一部分,那就更好了。另一种选择是有一个负责的管理器类。注意:即使是看似无辜的突变也需要锁定多线程应用程序。
  2. 此外,我建议避免将相等运算符用于数据结构并非绝对微不足道的类。很多时候,项目的进展会产生对比较策略(参数)的需求,而等式运算符的接口变得不够充分。在这种情况下,添加比较方法或仿函数不需要反映标准 operator== 接口以实现参数的不变性。
  3. 如果 1. 和 2. 对您的情况来说似乎过大了,您可以将 c++ 关键字 mutable 用于哈希值成员。这将允许您甚至从 const 类方法或 const 声明的变量修改它
于 2013-04-12T15:34:43.397 回答
1

是的,引入语义上意想不到的副作用总是一个坏主意。除了提到的其他原因:始终假设您编写的任何代码将永远只被甚至没有听说过您的名字的其他人使用,然后从这个角度考虑您的设计选择。

当使用您的代码库的人发现他的应用程序很慢并尝试对其进行优化时,如果它在 == 重载中,他将浪费时间试图找到性能泄漏,因为从语义的角度来看,他并不期望它看来,要做的不仅仅是简单的对象比较。在语义便宜的操作中隐藏潜在的昂贵操作是一种不好的代码混淆形式。

于 2013-04-13T00:31:21.667 回答
0

你可以走可变路线,但我不确定是否需要这样做。您可以在需要时进行本地缓存,而无需使用 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 保留为成员变量是必要的,而不是让它在需要的地方本地化?请注意,如果我们真的想要,我们可以进一步摆脱“静态”使用,我们要做的就是创建一个专用的结构/类

于 2013-04-12T22:06:30.587 回答