Search code examples
c++c++17

Building a predicate out of a predicate for nested containers


I'm trying to build an associative container with a customizable comparison predicate and I'm internally using a set to store some of the data. It should be pretty straightforward, I just need to pass along the predicate:

template <typename T, typename TComparePredicate  = std::less<T>>
class MyContainer
{
public:
    MyContainer(const TComparePredicate& predicate = TComparePredicate()) : _internalSet(predicate) { ... }
    ...
private:
    std::set<T, TComparePredicate> _internalSet;
};

Unfortunately, my real case is more complex. I need to store other data in the set (don't ask why, it makes sense in this context), so my code looks more like this:

template <typename T, typename V, typename TComparePredicate = std::less<T>>
class MyContainer
{
public:
    MyContainer(const TComparePredicate& predicate = TComparePredicate()) : _internalSet(???) { ... }
    ...
private:
    std::set<std::pair<T, V>, ???> _internalSet;
};

I still need the set to be ordered only considering the T value, so my simple solution is to create a functor that stores the real predicate and dispatches calls to it:

template <typename T, typename V, typename Predicate>
struct PairForwardingFunctor
{
    PairForwardingFunctor(const Predicate& predicate = Predicate()) : _predicate(predicate) { }
    bool operator()(const std::pair<T, V>& lhs, const std::pair<T, V>& rhs) { return _predicate(lhs.first, rhs.first); }

private:
    Predicate _predicate;
};

But I feel uneasy with this solution. I haven't still compiler-explored this (if it can even get down there), but I have a feeling there is a level of indirection that is not optimizable.

Is there a better approach to this?


Solution

  • While working on a similar problem, I found out a solution which should be a little more efficient, despite being more verbose:

    template <typename T, typename V, typename Predicate, typename = std::void_t<>>
    struct PairForwardingFunctor : private Predicate
    {
        PairForwardingFunctor(const Predicate& predicate = Predicate()) : Predicate(predicate) { }
        bool operator()(const std::pair<T, V>& lhs, const std::pair<T, V>& rhs) { return Predicate::operator()(lhs.first, rhs.first); }
    };
    
    template <typename T, typename V, typename Predicate>
    struct PairForwardingFunctor<T, V, Predicate, std::void_t<std::is_pointer<Predicate>::type>
    {
        PairForwardingFunctor(const Predicate& predicate) : _predicate(predicate) { }
        bool operator()(const std::pair<T, V>& lhs, const std::pair<T, V>& rhs) { return _predicate(lhs.first, rhs.first); }
    
    private:
        Predicate _predicate;
    };
    

    This avoids any internal object creation for lambda and function objects, which are way more likely to be optimized away. Function pointers are still stored, as I don't think it's possible to do otherwise with those.

    It's obviously still a stub, as it needs more care about transparent comparison, but if nobody has a better idea I'm going to choose this answer in a few days.