何时有必要为operator==和operator!=分别实现不同的实现?

3
我听说 C++ 可以重载 operator==operator!=,因为在某些情况下,a != b 的实现可以比 !(a == b) 更有效率。我思考了一下,但无法想象出这种情况。
有哪些例子可以使得分别实现 operator==operator!= 在性能或其他方面更加合理?

2
你不需要知道。可以说,可能会出现一些情况,其中你有一个非常明显的不等式条件,以及一个不太明显的等式条件。C++不会限制你只能根据等式或反之定义不等式。 - user229044
3个回答

4

我首先想到的例子是类似于SQL中的NULL值的实现。在这种情况下,比较两个对象——其中任何一个都是NULL——并不意味着它们相等。只有两者都不是NULL时,才有返回相等的意义。


1
我不清楚你在说什么。如果它们都是NULL,那么 operator== 应该返回什么?听起来你的意思是它应该返回false。这很好。但是你是否在说operator!= 也应该返回false?所以它们不相等,但它们也不相等? - Benjamin Lindley

3
如果您希望规则是a == b返回true时,a != b返回false,则确实没有理由有两种实现,除非您希望优化掉一个!。 (这很少发生变化,并且最好由优化器完成。)
但是,C++通常不假设运算符重载遵守这种规则。
例如,您可能还认为,只需要重载operator <,就可以获得operator >、operator <=、operator >=和operator ==。如果您认为它返回布尔值并且关系应该是部分顺序,则所有这些都可以用operator <定义。
但是,在某些情况下,运算符也用于提供更复杂的语法和语义。如果强加了这些“恒等式”,那么某些东西(例如表达式模板)就会变得不可能。
C++不会对您施加任何“恒等式”。您可以随意赋予操作符任何含义,无论是好是坏。
因此,我认为您所听到的可能是一种误解。您拥有这种自由的原因不是为了提供更多的“效率”机会,而是允许您在使用自定义类时给运算符赋予您想要的含义。
为了完整起见,这里是一个示例。
namespace expression_builder {

   struct arg {
     bool operator()(bool input) const {
       return input;
     }
   };

   template <typename E>
   struct negate {
     E e;

     bool operator()(bool input) const {
       return !e(input);
     }
   };

   template <typename E1, typename E2>
   struct equals {
     E1 e1;
     E2 e2;

     bool operator()(bool input) const {
       return e1(input) == e2(input);
     }
   };

   template <typename E1, typename E2>
   struct not_equals {
     E1 e1;
     E2 e2;

     bool operator()(bool input) const {
       return e1(input) != e2(input);
     }
   };

   // Operator overloads

   template <typename T>
   auto operator!(T t) -> negate<T> {
     return {t};
   }

   template <typename T1, T2>
   auto operator==(T1 t1, T2 t2) -> equals<T1, T2> {
     return {t1, t2};
   }

   template <typename T1, T2>
   auto operator!=(T1 t1, T2 t2) -> not_equals<T1, T2> {
     return {t1, t2};
   }

} // end namespace expression_builder

int main() {
  using expression_builder::arg;

  auto my_functor = (arg == (arg != (!arg)));
  bool test1 = my_functor(true);
  bool test2 = my_functor(false);
}

在这段代码中,使用运算符重载允许您构造函数对象以实现简单的布尔函数。函数构建过程完全发生在编译时,因此生成的代码非常高效。人们在更复杂的示例中使用它来在C++中非常有效地执行某些类型的函数式编程。这里非常关键的是operator ==operator !=有非常不同的实现。


1
在非常简单的情况下,“实现不等式中的相等”(或反之亦然)成语就足够了。在x86上,cmp指令用于判断等式和不等式。以以下示例为例:
struct Foo
{
  bool operator==(const Foo& rhs)
  {
    return val == rhs.val;
  }

  bool operator!=(const Foo& rhs)
  {
    return val != rhs.val;
  }

  int val;
};

Foo a{20};
Foo b{40};
Foo c{20};

int main()
{
  (void)(a == b);
  (void)(a == c);
  (void)(a != b);
  (void)(a != c);
}

这将编译为相同的汇编代码,除了 setesetne。人们可能会对分支预测、流水线、CPU缓存等进行琐碎的争论,但它们实际上只是无意义的陈述。
在复杂情况下,可能会诱惑人们为operator==operator!=赋予不同的语义,但我不同意这个原则:
- 您正在违反用户的期望,即这两个运算符是彼此的反义词。例如,!(a == b)a != b之间有什么区别吗?您很容易陷入其他语言(如PHP和Javascript)中等式是一种特殊的地狱的陷阱。 - 在操作符重载和迭代器后面隐藏复杂对象可能会严重影响性能。人们使用这些功能的假设是它们是便宜的(在大多数情况下确实如此)。昂贵的“迭代器”或“等式”使正确使用变得非常困难。 “何时有意义”更多地是由业务需求而不是适当的设计来回答的,不幸的是。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接