Slava Slava - 3 months ago 17
C++ Question

Transparent comparator code minimization

Let's say we store a struct with a string key and we want to find it by that string in a container like

std::set
, so common implementation would look like this:

struct Foo {
std::string id;
};

struct FooComp {
using is_transparent = std::true_type;

bool operator()( const Foo &foo, const std::string &str ) const
{
return foo.id < str;
}

bool operator()( const std::string &str, const Foo &foo ) const
{
return str < foo.id;
}

bool operator()( const Foo &foo1, const Foo &foo2 ) const
{
return foo1.id < foo2.id;
}
};

std::set<Foo,FooComp> foo_set;
...


This works fine, but writing three methods for
FooComp
that do prety match the same (logically) is monotonic and error prone. Is there a way to minimize that code?

Answer

You may do as following:

struct Foo {
    std::string id;
};

struct FooComp {
    using is_transparent = std::true_type;

    template <typename LHS, typename RHS>
    bool operator()(const LHS& lhs, const RHS& rhs) const
    {
        return ProjectAsId(lhs) < ProjectAsId(rhs);
    }

private:
    const std::string& ProjectAsId(const std::string& s) const { return s; }
    const std::string& ProjectAsId(const Foo& foo) const { return foo.id; }
};

You write comparison once, but you have to write the projection for each type.

In C++17, it can even be

template <auto f> struct ProjLess
{
    using is_transparent = std::true_type;

    template <typename LHS, typename RHS>
    bool operator()(const LHS& lhs, const RHS& rhs) const
    {
        return project(lhs) < project(rhs);
    }

private:
    template <typename T>
    using f_t = decltype(std::invoke(f, std::declval<const T&>()));

    template <typename T>
    using is_f_callable = is_detected<f_t, T>;

    template <typename T, std::enable_if_t<is_f_callable<T>::value>* = nullptr>
    decltype(auto) project(const T& t) const { return std::invoke(f, t); }

    template <typename T, std::enable_if_t<!is_f_callable<T>::value>* = nullptr>
    const T& project(const T& t) const { return t; }
};

And usage:

std::set<Foo, ProjLess<&Foo::id>> s;