什么时候对运算符==和运算符!=有单独的实现有意义?



我听说C++可以覆盖operator==operator!=,因为在某些情况下,可以实现a != b!(a == b)更有效。

我想过这个问题,无法想象这是真的。

有哪些示例在性能方面或其他方面为operator==operator!=单独实现是有意义的?

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

如果你想让规则a == b在返回 false 时准确地返回 truea != b那么实际上,没有理由有两个实现,除非你希望以某种方式优化单个!。(这很少会产生影响,最好由优化器完成。

但是,C++通常不会假设运算符重载遵守此类规则。

例如,您可能还会认为,您只需要重载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 !=非常不同。

在非常简单的情况下,"在不平等方面实现平等"(反之亦然)的习语就足够了。在 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,平等是一种特殊的地狱。

  • 将复杂对象隐藏在运算符重载和迭代器后面可能会严重损害性能。人们使用这些功能时假设它们很便宜(在大多数情况下它们很便宜)。昂贵的"迭代器"或"相等"使其很难正确使用。

不幸的是,"什么时候有意义"更像是一个由业务需求回答的问题,而不是适当的设计。

最新更新