0

I stumbled upon very strange compile error for std::set using transparent comparators with help of std::less. Consider this simple program:

using Key = std::string;

bool operator<(const Key&, int) { return true; }
bool operator<(int, const Key&) { return true; }

int main()
{
  std::set<Key, std::less<>> s;
  int x;
  auto it = s.find(x);
}

It gives me compile error:

error: no matching function for call to object of type 'const std::less<void>'
      if (__j != end() && _M_impl._M_key_compare(__k, _S_key(__j._M_node)))
                          ^~~~~~~~~~~~~~~~~~~~~~

If I use my own class instead of std::string as a key, it works fine:

struct My {};
bool operator<(const My&, const My&) { return true; }

using Key = My;

Why it does not work for std::string?

See the demo here: https://gcc.godbolt.org/z/MY-Y2s

UPD

What I really wanted to do, is to declare comparison operators between std::unique_ptr<T> and T*. But I thought it would be clearer with std::string.

Mikhail
  • 20,685
  • 7
  • 70
  • 146
  • `std::less`, no? –  Jan 25 '19 at 17:32
  • 1
    You're comparing apples to oranges. Your `My` implementation takes 2 `My`'s. You `string` implementation takes a `int` and a `string`. That requires the comparator be transparent. – NathanOliver Jan 25 '19 at 17:32
  • 1
    Is it allowed by standard to define an operator overload which doesn't involve any user provided type? – eerorika Jan 25 '19 at 17:32
  • @eerorika Don't see why not – Lightness Races in Orbit Jan 25 '19 at 17:33
  • 1
    @eerorika `std::string` is considered a user provided type. Although you should not overload operators for types you do not own. – NathanOliver Jan 25 '19 at 17:33
  • @eerorika: thirdly it may _also_ be failing because the Koening lookup doesn't see the compactors in the global namespace. – Mooing Duck Jan 25 '19 at 17:33
  • By the way if you really want to create these operators I'd make them functors that you have to name explicitly, or make `Key` a strong alias... otherwise this is kind of leaky. That can also fix the problem if it is indeed ADL (because you can put it it in the same namespace as your strong alias) – Lightness Races in Orbit Jan 25 '19 at 17:34
  • 1
    Possible duplicate of [What is "Argument-Dependent Lookup" (aka ADL, or "Koenig Lookup")?](https://stackoverflow.com/questions/8111677/what-is-argument-dependent-lookup-aka-adl-or-koenig-lookup) – JVApen Jan 25 '19 at 17:34
  • @Lightness I think it's allowed if you don't do it in namespace`std`. However that breaks ADL. Although, it might be undefined to allow the standard to add it later in std as it might break programs – JVApen Jan 25 '19 at 17:38
  • @LightnessRacesinOrbit let's say you use a library which happens to have made the same decision of defining their own `bool operator<(const std::string&, int)` and you end up violating ODR inadvertently. Given its fragility, I was hoping that there would be a rule disallowing such overloads for all standard types. – eerorika Jan 25 '19 at 17:52
  • @eerorika I agree that it would probably be better, on balance, were that a rule. Some way to enforce the rule that Nathan also mentioned (only overload operators for types that you own). In general though that is hard to diagnose. Certainly at the moment AFAIK there is no such constraint (which is what I meant by "don't see why not"!) – Lightness Races in Orbit Jan 25 '19 at 17:55
  • @LightnessRacesinOrbit They are working on it. We now have [SD-8](https://isocpp.org/std/standing-documents/sd-8-standard-library-compatibility) and hopefully this will become well know and followed. – NathanOliver Jan 25 '19 at 17:57
  • @NathanOliver Oh god finally! – Lightness Races in Orbit Jan 25 '19 at 18:30
  • @LightnessRacesinOrbit Yep. I actually learned about it from watching [this](https://www.youtube.com/watch?v=BWvSSsKCiAw). It's a good watch. – NathanOliver Jan 25 '19 at 18:33
  • `operator<(const std::string&, int)` is probably a bad idea. if it was a good idea, it would certainly already be standard since 98. This kind of comparison easily leads to **strict weak ordering** violations. – Julien Villemure-Fréchette Jan 25 '19 at 18:34
  • Pretty much any custom comparison easily leads to strict weak ordering violations, unless you design it properly. – Lightness Races in Orbit Jan 25 '19 at 18:37
  • @JulienVillemure-Fréchette I think it should be clear that the example is over-simplified from the fact my comparisons always return `true` :) – Mikhail Jan 25 '19 at 18:39

1 Answers1

2

Argument-dependent lookup's a funny old thing, isn't it?

There already exists a operator< relating to std::string in the namespace std, and this is found when looking for a < that fits your arguments. Perhaps counter-intuitively (but not without good reason), no further lookup is attempted after that! Other namespaces are not searched. That's even though only your overload actually matches both arguments. Your global operator< is effectively hidden in this context, as shown in the following horrendous example:

namespace N
{
    struct Foo {};

    bool operator<(Foo, Foo) { return false; }
}

bool operator<(N::Foo, int) { return false; }

namespace N
{
    template <typename T1, typename T2>
    bool less(T1 lhs, T2 rhs)
    {
        return lhs < rhs;
    }
}

int main()
{
    N::Foo f;
    N::less(f, 3);
}

/*
main.cpp: In instantiation of 'bool N::less(T1, T2) [with T1 = N::Foo; T2 = int]':
main.cpp:22:17:   required from here
main.cpp:15:20: error: no match for 'operator<' (operand types are 'N::Foo' and 'int')
         return lhs < rhs;
                ~~~~^~~~~
*/

(live demo)

Now, you can't add things to the namespace std, but that's fine because it would be vastly better if you didn't overload operators relating to other peoples' types anyway. That is the quickest way to hidden ODR bugs when some other library does the same thing.

Similarly, creating something called Key that is actually just std::string in disguise is a recipe for unintended conflicts and unexpected behaviours.

Overall I would strongly suggest making your Key at least a "strong alias" for std::string; that is, its own type rather than simply an alias. As you've found out, that also solves your problem because now the operator< and the operand type are in the same namespace.

More generally, if you are not really using an alias here but do indeed want to operate on standard types, you are back to writing a named custom comparator, which isolates the new logic very nicely and is also pretty much trivial to use. The downside of course is that you have to "opt-in" to it every time, but I think it's way worth it overall.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • I'm sure I've got this slightly wrong as ADL boggles my mind - someone feel free to edit if needed. But I think the spirit of the answer is right – Lightness Races in Orbit Jan 25 '19 at 18:28
  • Thank you so much for the detailed explanation. I completely agree that declaring `operator<` as I did it does not make much sense. But, as you can guess, this is a simplified example. What I really wanted to do is to introduce comparisons between `std::unique_ptr` and `T*`. This setup produces exactly the same error. I can, of course, hide comparison operators for those in a custom transparent comparator, but I am just interested how do I do it without custom comparators. Say, I don't want to provide them in every associative container I use. Any ideas on that?.. – Mikhail Jan 25 '19 at 18:30
  • @Mikhail Not off-hand; custom comparators seem like the obvious and best solution to that. I do understand the temptation to get this behaviour "transparently", but I think that may be a _dark_ temptation because that transparency is liable to lead you down the path of evil. If you could even get it to work, that is. – Lightness Races in Orbit Jan 25 '19 at 18:31
  • Well, it is unclear to me why we don't have comparisons between smart pointers and raw pointers out of the box in the first place. BTW, I can get it to work if I put comparison operators into namespace `std` :3 – Mikhail Jan 25 '19 at 18:41
  • @Mikhail Well, it's probably going to surprise you to discover that [the result of `p1 < p2`, if they don't point to the same object or array, is unspecified](https://stackoverflow.com/a/31774802/560648). You shouldn't be treating pointers like numbers, just because their implementation is typically (okay, fine, always) a numeric address-like thing. So if we can't compare arbitrary pointers, it's not a surprise that the standard doesn't give us a way to compare an arbitrary raw pointer with an arbitrary smart pointer. However, it could be valid, if you got the raw pointer from `.get()`. – Lightness Races in Orbit Jan 25 '19 at 18:48
  • @Mikhail _"BTW, I can get it to work if I put comparison operators into namespace std"_ But you're not allowed to do that. The compiler will let you, but the standard will not. – Lightness Races in Orbit Jan 25 '19 at 18:53
  • _"it's probably going to surprise you to discover that the result of p1 < p2, if they don't point to the same object or array, is unspecified"_ - it is not, I am aware of that, and in my implementation of comparisons I used `std::less`, which is required to imply total order. And which `unique_ptr::operator<()` uses. But thank you for mentioning, it is indeed a very subtle property of pointers. – Mikhail Jan 25 '19 at 19:17
  • _"But you're not allowed to do that."_ - I understand that, I hoped the smile will say it for me :) But again, thank you very much for mentioning! – Mikhail Jan 25 '19 at 19:18