关于c ++:运算符==改变其操作数是不好的做法?

Is it bad practice for operator== to mutate its operands?

脚本

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

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==函数被认为与其众所周知的语义相反,如果该变异没有任何可观察到的影响(因为用户无法看到散列的内容)?

  • 如果上述任何一个的答案都是"是",那么什么是更合适的方法呢?


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

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


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

    你现在,'变异” → mutable

    然后根据逻辑const -ness进行思考,保证对象提供给使用代码。


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


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

    当有人使用你的代码库发现他的应用程序很慢,并试图优化它时,如果它在==重载内,他将浪费很多时间试图找到性能泄漏,因为他不期望它,从语义点对于视图来说,要做的不仅仅是简单的对象比较。在语义上廉价的操作中隐藏可能代价高昂的操作是一种糟糕的代码混淆形式。


  • 不建议在比较函数或运算符中具有副作用。如果您可以设法计算哈希作为类初始化的一部分,那将会更好。另一个选择是拥有一个负责这个的经理类。注意:即使是看似无辜的突变也需要在多线程应用程序中锁定。
  • 另外,我建议避免对数据结构不是绝对平凡的类使用相等运算符。通常,项目的进度需要比较策略(参数),并且相等运算符的接口变得不足。在这种情况下,添加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作为成员变量是必要的,而不是让它在需要的地方?请注意,如果我们真的想要的话,我们可以进一步摆脱"静态"使用,我们所有的东西都是做一个专门的结构/类