c++stdset

How do I use std::set with a comparator that applies a projection to the key?


Suppose I have a set (or map) of strings, and I want to use a custom comparator that compares only the first 5 characters. So "abcde" and "abcdef" are the same in my set.

using MySet = std::set<std::string, Cmp>;

What is the best way to write Cmp?

The obvious way is like this:

struct Cmp
{
    bool operator()(const string& x, const string& y) const
    {
        return x.substr(0, 5) < y.substr(0, 5);
    }
};

The problem is that this code repeats .substr(0, 5). In this example it's pretty short, but in the general case it could be longer. I want to avoid this repeating code.

In general, given the types T1, T2 and the function T2 key(T1& const), I want a set of T1 elements that compares according to key(a) < key(b), where comparison on T2 is already well-defined. What is the best way to write this? I thought about writing a new class KeyBaseSet, but that would be over-designing for my single use-case. Is there some way to do this using std or Boost?

I'm looking for something similar to the key argument when sorting in Python (https://docs.python.org/3/howto/sorting.html#key-functions), or the compare `on` idiom in Haskell (https://stackoverflow.com/a/2788262/351105).


Solution

  • You can customize Cmp with a key policy. Minimal example:

    template<class Key>
    struct Compare_on {
        Compare_on(Key key = Key()) : key_(key)
        {}
    
        template<class T>
        bool operator()(const T& x, const T& y) const {
            return key_(x) < key_(y);
        }
    
    private:
        Key key_;
    };
    
    struct First3 {
        std::string_view operator()(const std::string& s) const {
            return std::string_view(s).substr(0, 3);
        }
    };
    
    // Example:
    std::set<std::string, Compare_on<First3>> set;
    set.insert("abc1");
    set.insert("abc2");
    

    Demo


    Compare_on can be improved by making it a transparent comparator:

    template<class Key>
    struct Compare_on {
        using is_transparent = void;
    
        Compare_on(Key key = Key()) : key_(key)
        {}
    
        template<class T1, class T2>
        bool operator()(const T1& x, const T2& y) const {
            return key_(x) < key_(y);
        }
    
    private:
        Key key_;
    };
    
    struct First3 {
        template<class T>
        std::string_view operator()(const T& s) const {
            return std::string_view(s).substr(0, 3);
        }
    };
    

    Now when we do

    auto pos = set.find("abc");
    

    no temporary std::string will be constructed for the string literal "abc".

    Demo 2