什么时候对运算符==和运算符!=有单独的实现有意义?
When does it make sense to have separate implementations for operator== and operator!=?
我听说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);
}
这将编译为相同的程序集,没有sete
与setne
。人们可能会分裂头发,并对分支预测、管道、CPU 缓存等做出模糊的断言。但它们实际上只是空洞的陈述。
在复杂的情况下,可能很容易为operator==
和operator!=
提供不同的语义,但我不同意这个原则:
您违反了用户的期望,即这两个运算符彼此相反。例如,
!(a == b)
或a != b
之间有什么区别吗?你很容易落入其他语言的陷阱,如PHP和Javascript,平等是一种特殊的地狱。将复杂对象隐藏在运算符重载和迭代器后面可能会严重损害性能。人们使用这些功能时假设它们很便宜(在大多数情况下它们很便宜)。昂贵的"迭代器"或"相等"使其很难正确使用。
不幸的是,"什么时候有意义"更像是一个由业务需求回答的问题,而不是适当的设计。
- 如何正确实现和访问运算符的各种自定义枚举器
- 三向比较运算符成员与非成员实现
- C++矩阵类运算符使用 std::common_type_t 和复数的实现
- 如何为我的类实现/重载二进制运算符
- 默认赋值运算符如何在实际 STL 中实现
- 使用'+='运算符统一实现 C++ '+'运算符
- 运算符+ 的规范实现涉及额外的移动构造函数
- 如何在层次结构中实现运算符使用?
- 如何在C++中实现切片运算符[]?
- C++ 20 中的运算符 == 和 <=> 应该作为成员还是自由函数实现?
- 将赋值运算符实现为"destroy + construct"是否合法?
- 为模板类中的>>运算符和<<运算符实现重载
- 使用括号运算符实现矩阵类的安全方法
- 分配运算符实现的说明
- 为什么 std::sort 不使用我的运算符<实现
- 复制构造函数和赋值运算符实现选项 -
- 如何在最大限度地提高大小有效性的同时,将3态的位运算符实现为任何大小的内存
- 将比较运算符"运算符<"实现为成员函数或外部函数
- 如何使用+=运算符实现标量和向量相加
- 为什么运算符>由 C++ STL 库中的运算符* 实现?