Getting in trouble with mixed comparisons
Post
Cancel

# Getting in trouble with mixed comparisons

Andrzej has a great new post about the difficulty that arises when the language or the library tries to make assumptions about programmer intent. He brings up two examples of this difficulty:

• should CTAD wrap or copy?
• how should `optional`’s mixed comparison behave?

I’ve touched on the first topic in an earlier post on CTAD quirks, and I wanted to write some words about the latter topic here - since I think it’s really interesting.

Mixed comparisons are very closely related to implicit conversions, which themselves are always going to be a difficult topic. Let’s just start on the low end and work our way up, with just the `int`s:

``````bool f1(int a, long b) {
return a == b;
}
``````

Unless you take the view that implicit conversions are always wrong, and thus that mixed conversions should never happen (which is an extreme view in terms of the range of possible opinions to have, but isn’t totally outrageous), this is a perfectly reasonable function. `a` and `b` have different types, but represent the Same Platonic Thing. We can convert from `int` to `long` both cheaply and without any information loss. There aren’t any weird caveats with comparing an `int` to a `long`. It just works and does the sensible thing.

Let’s go one step outwards:

``````bool f2(optional<int> a, optional<int> b) {
return a == b;
}
``````

The only reason to reject this example would be if you think that `optional<T>` shouldn’t have any comparisons at all under any circumstances. I don’t know of anybody who has expressed that view. This function also has a clear and sensible meaning, `a == b` if either `a` and `b` are both disengaged or both are engaged with the same underlying `int` value.

Let’s go one step further:

``````bool f3(optional<int> a, optional<long> b) {
return a == b;
}
``````

This one is more interesting. The underlying comparison here is equivalent to `f1()`: either both `optional`s are disengaged or both are engaged and we do the mixed-integer comparison. If we think that comparison is sensible, surely this one should be as well? We’ve added a wrapping layer here, but we haven’t actually added any kind of additional semantics that would break our notion of Same Platonic Thing.

``````bool f4(optional<int> a, int b) {
return a == b;
}
``````

This is a different kind of mixed comparison than the ones in `f1()` and `f3()`. But I think you can still make the argument that this comparison has an obvious meaning: `a` is engaged and its underlying value is the same as `b`. This is a perfectly reasonable semantic, and there really is no other meaning this comparison could take on. Outside of being ill-formed, that is. Since `optional<int>` is implicitly constructible from `int`, if `optional<T>`’s comparison operators as declared as non-member friends, then you get this comparison even without asking for it.

``````bool f5(optional<int> a, long b) {
return a == b;
}
``````

If you allow `f3()` (a mixed-optional comparison) and `f4()` (an optional-value comparison), then you must allow this one right? Arguably, we’re still making perfectly sensible decisions with each step here. Every comparison presented thus far does have one, clear semantic meaning - with no weird quirks, caveats, or edge cases.

So far, so good right?

But what happens when we do this:

``````bool f6(optional<optional<int>> a, optional<int> b) {
return a == b;
}
``````

Think about this for a minute. In particular, what should the value of `f6(nullopt, nullopt)` actually be? This is basically the case that Andrzej brought up at the end of his post. It’s tempting to say the answer is obvious, but it’s surprisingly not. There are two different ways of thinking about this:

1. This is a special case of `f3()`: comparing `optional<T>` to `optional<U>` for types `T` and `U` that are comparable (in this case `T=optional<int>` and `U=int`, which is the `f4()` comparison). If we think about it in these terms, then the result of `f6(nullopt, nullopt)` should be `true` because we have two disengaged `optional`s, so they are equal.
2. This is a special case of `f4()`: comparing `optional<T>` to `T` for type `T` that is comparable (in his case `T=optional<int>`). If we think about it in these terms, then the result of `f6(nullopt, nullopt)` is `false` because the `a` is disengaged - so it cannot compare equal to a value.

The Standard Library picks option 1 by way of allowing mixed-optional comparisons. Boost does not support mixed-optional comparisons (`f3()` does not compile using `boost::optional`), so it picks option 2. I’m not sure either of these options is more defensible than the other.
A third different, arguably defensible implementation strategy would be to allow mixed-optional comparisons (like `f3()`) but disallow mixed-category comparisons (like `f4()`), in which case `f6()` wouldn’t compile. There is an open source implementation that makes this choice (Basit Ayantunde’s SKX) and it has its own inconsistency: `f4()` does not compile, but if the body of `f4()` used `f2(a,b)` instead of `a == b`, it would have? But if you disallowed implicit conversions (that is, `optional<int>`’s value constructor is `explicit`), then allowing `f3()` but disallowing both `f4()` and `f6()` would be both reasonable and consistent.
What’s the moral of this post? I am not sure. Even making a couple of really sensibly and easily defensible decisions (allowing comparing `optional<int>` to both `long` and `optional<long>`) still leads us to a situation where there just is no right answer. What did the programmer actually want `f6()` to mean? In this case, the programmer may not even know what they wanted! Is that a reason in of itself to view this as a library defect and try to prevent this particular kind of comparisons? I do not know. Ultimately, I just thought this was an interesting dilemma and wanted to share.