比较不同类型的对象是否被视为良好的设计?

7
你认为这是否证明设计不好?
//FooType and BarType not in the same hierarchy
bool operator==(const FooType &, const BarType &);
bool operator<(const FooType &, const BarType &);

例如,如果FooType是以自纪元以来的秒数为单位的double类型,而BarType是提供UTC日期的三个整数(年、月和日)的元组,则上述比较“有意义”。
你见过这样的类型间比较吗?在C++社区中,它们是否被看作是不好的做法?

3
取决于谁将维护代码 :) 我倾向于将其实现为类方法,即BarType::CompareToEpoch。顺便说一句,用long int来衡量自纪元以来的秒数比double更好... - Alex1985
我认为这个问题被搁置的原因将会是你的答案。 - ChiefTwoPencils
我认为是的,因为operator==(const FooType&)应该是BarType类的一个方法,而不是一个函数。 - chepner
考虑为另一种类型编写转换构造函数。即使使用类似于接受天、月和年类型的构造函数,您仍然可以执行 if (time == {Day(13), Month(5), Year(1996)}) 的操作。 - chris
@Alex1985,一个双精度浮点数可以容纳不超过2 ** 53的整数,而不需要四舍五入,这为您提供了284836042年的秒数。这不够吗? - Mark Ransom
@chepner:嗯,对称二进制运算符应该是自由函数,而不是成员。如果你只想在类定义中看到声明,请使用“friend”。 - aschepler
4个回答

4
首先,使用自由函数而不是成员函数没有问题,事实上这是推荐的做法。参见Scott Meyer的How Non-Member Functions Improve Encapsulation。但你需要提供双向比较:
bool operator==(const FooType &, const BarType &);
bool operator==(const BarType &, const FooType &);

其次,如果比较有意义的话,提供这些比较是完全可以接受的。例如,标准库允许您将`std::complex`值与浮点数进行相等性比较,但不能进行小于比较。
你要避免的唯一一件事就是没有意义的比较。在你的示例中,其中一个时间值是double类型,这意味着一旦考虑了标准提升,比较将针对任何浮点数或整数值进行。这可能超出了您的意图,因为无法确定任何特定值是否表示时间。类型检查的丢失意味着存在意外错误的潜在风险。

3

个人观点和经验

我个人并不反对不同类型之间的比较。我甚至鼓励这样做,因为它可以提高代码的可读性;使你所做的事情更加合乎逻辑。除了基本的数字类型,可能还有一个字符串和一个字符,我发现很难给出一个逻辑上的内部类型比较,并且我不记得曾经遇到过很多这样的情况。但是我遇到过很多算术运算符被用作这样的情况。

如何使用它们

你应该谨慎你所做的事情,它们很少被使用是有原因的。如果你提供一个比较两种不同类型的函数,结果应该是逻辑的,并且用户直观地期望这样做。为此编写良好的文档也是可取的。Mark Ransom已经说过了,但是如果用户可以双向比较,那就太好了。如果你认为你的比较不够清晰,可以考虑使用一个命名函数。如果你的操作符可以有多重含义,这也是一个非常好的解决方案。

可能出现的问题

你无法完全控制用户将如何处理你编写的内容。tletnes就给出了一个很好的例子,其中比较了两个整数,但结果没有意义。与此相反,两种不同类型的比较可能非常正确。一个表示秒的浮点数和一个表示秒的整数可以很好地进行比较。

算术运算符

除了逻辑运算符,我想展示一个使用算术运算符的内部类型示例。当谈论内部类型使用时,算术运算符就像逻辑运算符一样。

假设你有一个用于二维向量和正方形的+操作符。这是什么意思?用户可能认为它会缩放正方形,但另一个用户确信它会平移!这些问题对你的用户来说可能非常令人沮丧。你可以通过提供良好的文档来解决这个问题,但我个人更喜欢具体命名的函数,比如Translate。

结论

内部类型逻辑运算符可以很有用并使代码更简洁,但不良使用会使一切变得更加复杂。


2
需要注意的一点是,在“What can go wrong”部分中,不同类型所支持的比较数量可能会导致支持这些操作所需的函数数量迅速增加。2种类型需要4个函数(a==a,a==b,b==a,b==b)。3种类型需要9个函数。这是N^2复杂度(假设具有自反和对称性质)。 - Suedocode

1

良好的设计应该仅比较具有兼容含义的值。通常,类型是意义的良好线索,但不是最后的决定因素。事实上,在许多情况下,两个相同类型的值可能具有不兼容的含义,例如以下两个整数:

int seconds = 3 //seconds
int length = 2; //square inches
if(seconds >= length){
    //what does this mean?
}

在这个例子中,我们将长度与秒进行比较,然而两者之间没有有意义的关系。
int test_duration = 3 //minutes
float elapsed_time = 2.5; //seconds
if((test_duration * 60) >= elapsed_time ){
    //tes is done
}

在这个例子中,我们比较了两个不同类型(和单位)的值,但它们的含义仍然是兼容的(它们都代表时间),因此(假设有一个很好的理由为什么这两个值被存储在那里(例如使用API的便利性等),这是一个好的设计。)

0
根据Stepanov的规则(参见《编程元素》),相等性与复制(构造和赋值)(以及不等式)紧密相关。
因此,如果对象表示相等的值,则可以进行相等比较,但同时考虑这四个操作(相等性、[复制]构造、赋值和不等式)。此外,还包括从不同类型之间的转换(强制类型转换或另一侧的构造)。
它也隐含地与您可以应用于这些值的任何“常规”函数相关联。 Stepanov定义两个值相等,如果对它们应用任何(常规)函数都会产生相等的结果。
我想说的是,即使您可以将两个对象进行比较并相互构造,如果您可以应用于两者的公共函数集(通用或非通用)不是一个相关集合或它们的结果通常会产生不相等的值,那么比较不同类型的对象的价值就很小了。更糟糕的是,如果其中一个类型具有比另一个类型更多的函数,那该怎么办?能够保持自反性吗?

最后还要考虑算法复杂度,如果比较两个对象的复杂度为O(N^2)或更高(其中N是某种度量下的“大小”),那么可以理解为根本没有比较对象的价值。(参见John Lakos的演讲https://www.youtube.com/watch?v=W3xI1HJUy7Q

因此,如您所见,它不仅仅是提出一个比较标准来填写operator==的主体部分,或者是否是一种良好的实践,而这只是开始。等式非常基础,渗透到程序的所有含义中。


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