运算符==对其操作数进行变异是不是一种糟糕的做法

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

本文关键字:一种 操作数 是不是 变异 运算符      更新时间:2023-10-16

场景

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

示例

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。这是否有任何潜在的负面后果(例如,标准库函数或STL容器可能期望operator==具有const操作数(?

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

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

对于mutable成员来说,这似乎是一个非常有效的用例。您仍然可以(也应该(让operator==通过const引用获取参数,并为该类指定一个mutable成员作为哈希值。

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

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

U现在,";突变"→mutable

然后从逻辑const-ness的角度来思考,是什么保证了对象为正在使用的代码提供。

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

  1. 不建议在比较函数或运算符中产生副作用。如果您能够设法将散列计算作为类初始化的一部分,则会更好。另一种选择是有一个管理器类来负责这一点。注意:即使看似无害的突变也需要在多线程应用程序中进行锁定
  2. 此外,我还建议避免在数据结构并非绝对琐碎的类中使用相等运算符。通常情况下,项目的进度会产生对比较策略(参数(的需求,并且相等运算符的接口变得不足。在这种情况下,添加比较方法或函子将不需要反映参数不变性的标准运算符==接口
  3. 如果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作为一个成员变量,而不是将其本地化到需要的地方吗?请注意,如果我们真的想的话,我们可以进一步摆脱"静态"使用,我们所要做的就是制作一个专用的结构/类

相关文章: