30
#include <set>
#include <string>
#include <string_view>

using namespace std;

int main()
{
    string_view key = "hello";

    set<string> coll1;
    coll1.find(key); // error

    set<string, less<>> coll2;
    coll2.find(key); // ok since C++14
}

Then, should it be a rule:

Always prefer set<T, less<>> to set<T> since C++14?

xmllmx
  • 39,765
  • 26
  • 162
  • 323

2 Answers2

19

It's trivial to find a counterexample:

#include <set>
#include <string>

using namespace std;

struct converts_to_string {
    operator string() const { return ""; }
};

int main()
{
    converts_to_string key;

    set<string> coll1;
    coll1.find(key); // OK

    set<string, less<>> coll2;
    coll2.find(key); // error
}
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • 2
    ... what's the reason why it doesn't work? Is it because the Standard does not require (forbid?) `operator<` on `basic_string` to be a non-member function, and therefore deduction fails? Would it work if `operator<` for `basic_string` was defined as a friend (non-template) function? – dyp Dec 12 '16 at 10:12
  • 2
    @dyp Yep, deduction failure because it's a non-member function template (and unlike `basic_string_view` doesn't have a "sufficient additional overload" rule). And yes, a friend non-template function would make this work (at the cost of one temporary string per comparison, just like your `stupid_string`). – T.C. Dec 12 '16 at 12:53
10

There can be a performance downside when using associative_container<T, less<>>: Consider a type like

#include <iostream>
#include <set>
#include <string>

struct stupid_string
{
    stupid_string(char const* s)
      : s(s)
    { std::cout << "copy\n"; }

    stupid_string(char const* s, int) // silent
      : s(s)
    {}

    friend bool operator<(stupid_string const& lhs, stupid_string const& rhs);

private:
    std::string s;
};

bool operator<(stupid_string const& lhs, stupid_string const& rhs) {
    return lhs.s < rhs.s;
}

int main() {
    std::set<stupid_string, std::less<>> s;
    s.emplace("hello", 0);
    s.emplace("world", 0);
    s.emplace("foobar", 0);
    std::cout << "find\n";
    (void)s.find("test");
}

Here, the application of operator< in the algorithm performed by s.find will convert the character literal to a stupid_string implicitly. This happens for each comparison performed! Live demo

I know of one case where something similar happened in production code, with a non-conforming C++03 StdLib implementation.


This is by the way the main reason why heterogeneous lookup via less<> was made opt-in; see N3657:

Stephan T. Lavavej suggested that the two problems of preserving existing behaviour and allowing heterogeneous lookups could both be solved by making the containers detect when the comparison object accepts heterogeneous arguments and only conditionally overloading the current lookup functions with template versions.

dyp
  • 38,334
  • 13
  • 112
  • 177