【问题标题】:Is it bad practice for operator== to mutate its operands?operator== 改变其操作数是不好的做法吗?
【发布时间】:2013-04-05 04:35:10
【问题描述】:

场景

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

示例

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

背景

根据this answer上的指导,等式运算符的规范形式是:

inline bool operator==(const X& lhs, const X& rhs);

此外,以下关于运算符重载的一般建议is given

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

问题

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

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

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

【问题讨论】:

  • 我怀疑有 99% 的 X 存在堆栈溢出问题,其形式为“X 是一种不好的做法吗?”是不好的做法。问这个问题的人已经在他们的代码中做了 X 并且正在寻找道德支持以将其留在那里。其他 1% 的人提出了一些出色的新做法,只是在炫耀。
  • 为什么类声明中有friend 函数?为什么对类声明中定义的函数使用显式inline== 运算符只能是成员。左侧是隐式的(this 对象)。 bool operator == (const Foo &rhs) { ... }.
  • 如果getHash 函数实际上位于成员对象或基类上怎么办?假设您从某个具有 gethash 函数的 widget 类派生。你是否关心gethash 执行惰性实例化,所以只要它有效,它就具有破坏性?
  • 这个问题可以理解为什么 google c++ 风格指南不鼓励运算符重载。 google-styleguide.googlecode.com/svn/trunk/…
  • 我对该答案添加了评论。

标签: c++ comparison operator-overloading constants equality


【解决方案1】:

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

【讨论】:

  • 您的意思是在每个Foo 旁边保留一个HashCompare,然后每次都在本地检查,或者有一个免费的功能来做到这一点?这打破了封装(isHashed 是Foo 的属性,那么为什么它存在于Foo 之外?)并且意味着我必须将它们成对传递到需要它们的地方。有什么好处?如果我误解了您的建议,请澄清。
【解决方案2】:

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

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

【讨论】:

  • 有趣的一点,与Google style guide 中提到的相同,链接到其他 cmets 之一。在大多数情况下,散列旨在 减少 == 操作的成本,但您是对的,在某些情况下情况并非如此,例如用户从不多次比较对象。所以你建议我应该更喜欢一个命名的相等成员函数(例如bool equals(Foo&amp; rhs)),并在那里记录性能语义?
  • 是的,这样会更干净,让 == 运算符只做简单的、可能昂贵的简单比较。如果将 4GB 二进制文件放入其中,编码人员就会知道该操作的计算量很大。
  • 嗯?所以首先你说== 应该在计算上很便宜,因为它在语义上很便宜,现在你说它不应该被优化。 — 我想你的意思是,== 的计算成本应该是可预测的,但只要你有一个固定且合理的最坏情况界限,我看不出性能变化有什么问题.有许多操作可能需要不同的时间,具体取决于难以预测的情况,例如 std::vector::push_back 平均速度非常快,但有时需要重新定位整个数组。
  • 是的,措辞并不像它可能的那样干净,呵呵。我的意思是,比较的计算成本在最坏的情况下应该与其操作数的大小具有可预测的线性关系。引入副作用可能使其成倍增长。
  • 为什么会变成指数级?
【解决方案3】:
  1. 不建议在比较函数或运算符中有副作用。如果您可以设法在类的初始化过程中计算哈希值,那就更好了。另一种选择是有一个负责的管理器类。注意:即使是看似无害的突变也需要锁定多线程应用程序。
  2. 此外,我建议避免对数据结构并非绝对微不足道的类使用相等运算符。很多时候,项目的进展会产生对比较策略(参数)的需求,而相等运算符的接口变得不够充分。在这种情况下,添加 compare 方法或仿函数不需要反映标准 operator== 接口以实现参数的不变性。
  3. 如果 1. 和 2. 在您的情况下显得过于矫枉过正,您可以将 c++ 关键字 mutable 用于哈希值成员。这将允许您甚至从 const 类方法或 const 声明的变量修改它

【讨论】:

    【解决方案4】:

    您不应该在比较时修改对象。但是,此函数不会在逻辑上修改对象。简单的解决方案:使hash 可变,因为计算哈希是一种兑现形式。看: Does the 'mutable' keyword have any purpose other than allowing the variable to be modified by a const function?

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

      • 因为它没有被提及——如果你打算使用mutable 来执行缓存,建议以线程安全的方式这样做,因为否则@ 987654328@ 修改了Foo 可以通过同时从两个线程在同一个对象上调用它来观察。即使您从未在代码中明确执行此操作,it is the opinion of at least some C++ committee members 标准库执行此操作也是合法的。
      • @Mankarse 感谢您的评论和链接。
      • 请注意,您可以通过使用 0 作为标记散列并执行 if(atomic_load(&amp;hash)!=0)atomic_store(&amp;hash,calc_hash()); 使其成为线程安全的
      【解决方案6】:

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

      你现在,“变异”→mutable

      然后考虑逻辑const-ness,什么保证对象提供给使用代码。

      【讨论】:

        猜你喜欢
        • 2011-08-06
        • 2015-03-27
        • 1970-01-01
        • 2019-06-19
        • 1970-01-01
        • 1970-01-01
        • 2021-12-02
        • 1970-01-01
        • 2011-08-13
        相关资源
        最近更新 更多