Boost logo

Boost :

Subject: Re: [boost] [optional] operator<(optional<T>, T) -- is it wrong?
From: Gavin Lambert (gavinl_at_[hidden])
Date: 2014-11-27 17:44:50


On 28/11/2014 05:12, Gottlob Frege wrote:
> On Wed, Nov 26, 2014 at 6:49 PM, Gavin Lambert <gavinl_at_[hidden]> wrote:
>> TBH, other than the "existing code" issue I'm not sure I like the idea of
>> std::less working but op< not -- and in particular if you don't agree with
>> the idea that "none is less than any other value" then I don't know why
>> you'd agree with the idea that it should sort that way in a map, which is
>> what you appear to be suggesting.
>
> I don't like that std::less isn't the same as op< either, but it is
> the best we currently have. I hope to propose a fix for that -
> something like std::order, which map and set would use instead of
> std::less (and std::order would default to std::less for backwards
> compatibility).

I'm not sure I understand the meaning of having an order that isn't
implied by op<. If op< is omitted because comparison is meaningless or
otherwise confusing, how could it be well-defined how to sort them? If
there is not a single well-defined sort order, then the library should
not just pick one arbitrarily, and if there is, then why would it not be
appropriate as an op<?

> A good example of where std::order shouldn't be the same as op< would
> be std::complex. There isn't a sensible/natural mathematical way of
> answering whether one complex number is larger than the other (while
> at the same time getting the other properties we commonly expect from
> a less-than).
>
> So std::complex shouldn't have op<. But it would be nice to use them
> in maps and other algorithms. (Sure, we now have unordered_map, but
> map/set still has uses.)
>
> And std::complex, looked at as a pair<Number, Number>, does have a
> natural (lexicographical) ordering.
>
> So it would be nice if std::complex specialized std::order, but didn't
> have op<. If we don't get std::order, complex will probably
> specialize std::less in the near future, because it is what works
> today.

I don't agree with any of that. If you want to use it as a key for
something map/set-like, use unordered_map/set. I can think of multiple
possible orderings of std::complex depending on application need, and I
don't think any one of them is inherently "better" than any other. So
this should be left to the application to define the sort order, in the
rare case that this would actually be needed.

>> But why do you want to have optional keys anyway? When does it make sense
>> to have a single value attached to an empty key? It's rare for a dictionary
>> type to permit null keys in any language.
>>
>
> optional<T> is Regular if T if Regular. The more that optional<T>
> "just works" like int works, the better. Particularly for generic
> code.

The first part sounds like a definition I am unfamiliar with. I agree
with the second part, but I don't think it's relevant to this
discussion. Nobody is talking about removing
op<(optional<T>,optional<T>), which is what generic code would use. Any
generic code that *could* use both T and optional<T> must by definition
be aware of the fact that it's using optional, so will either know that
op<(optional<T>,T) is not sensible and avoid using it or will explicitly
convert as needed so that it invokes op<(optional<T>,optional<T>)
instead. Alternatively it must be aware that it is using different
types, so op< may not be sane between them. Either way I don't think
this would be a problem in real code.

However possibly there might need to be a type trait specialization that
explicitly denies op<(optional<T>,T) and the reverse, to avoid confusing
code that attempts to detect when op< does work between distinct types.

> For less-than we have these competing points, (that each of us may
> weigh differently)
>
> 0 - std::less should be same as op<
> (I think we all agree here, actually, which is why we should have std::order)
>
> 1 - op<(optional<T>, optional<T>) is "sensible" but somewhat arbitrary
> 2 - op<(optional<T>, T) is questionable, often (usually?) wrong
> 3 - in general, func(optional<T>, optional<T>) should work if passed
> T's (ie implicit conversion is important)
> 4 - op<(optional<T>, optional<T>) *is* of the form func(optional<T>,
> optional<T>) - ie why should it be different?

All true.

> 2 and 3, to me, are the strongest, but directly opposed.
> How do you solve that?
> Either say
>
> A1. "well operators are special, not typical functions" so we can
> poison them while keeping general functions implicit
> A2. 1 is on shaky ground, so let's remove both 1 and 2, but keep
> std::less/std::order, which was the main reason for having op<.

Of these, I prefer A1. I think it really is a special case. But I can
understand why the present behaviour is as it is.

I suspect those who dislike "none" having a defined order would go for
A2 though. I would be ok with that as well, but I don't have a problem
with this definition.

But you're leaving out A3: remove 1+2 AND std::less/std::order. This
means that optional types can't be used in map keys, only in
unordered_map. I actually think that this is the best option, except
that it has a greater potential to break existing code. Perhaps a
predicate could be defined specific to optional giving the current
ordering, so that users who do want that ordering or to continue using a
map could just specify that predicate as a minimal fix. But I don't
think that the predicate should be spelled "std::less".

(When I define custom ordering for reference types in C#, which are
nullable, I always specify that 'null' is smaller than any non-null
value as well. The library Nullable type behaves similarly, although as
I noted in another thread it does *not* support op< locally, you have to
explicitly call a static comparison method to compare two Nullable
values, or a Nullable and an implicitly promoted base value. This
aligns best with A3+predicate.)


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk