简体   繁体   中英

Dynamically selecting the comparison functor to be used in std::map

I have a set of data which needs to be sorted by a criteria that is decided by the user in runtime.

Ideally this sorting criteria has to be passed as a parameter in a function, such as:

void mainFunction(
    InputData const &inputData,
    OutputData &outputData,
    BaseSortingCriteria const &compareFunctor)
{

    /* The data is sorted by this map using the custom provided functor as criteria */
    std::map<InputDataType, ValueType, BaseSortingCriteria> sortedSets(compareFunctor);

    ...
}

To get this, I've created a virtual functor representing the base criteria, such as:

struct VirtualSortingCriteria
{
    virtual bool operator()(
        const InputDataType &var1,
        const InputDataType &var2) const = 0;
}

And to keep a common interface, a base functor, which simply executes a "real" functor passed during construction:

struct BaseSortingCriteria
{
    BaseSortingCriteria(
        std::shared_ptr<const VirtualSortingCriteria> pCompareFunctor) :
        m_realCompareFunctor(pCompareFunctor)
    {
    }

    bool operator()(
        const InputDataType &var1,
        const InputDataType &var2)
    {
        return m_realCompareFunctor->operator()(var1, var2);
    }

private:
    /** Pointer to the real functor to be used. */
    std::shared_ptr<const VirtualSortingCriteria> const m_realCompareFunctor;
}

And I've defined a couple of "real" functors to test:

struct RealFunctorVersionA final : public VirtualSortingCriteria
{
    bool operator () (
        InputDataType const &var1,
        InputDataType const &var2) const;
};

struct RealFunctorVersionB final : public VirtualSortingCriteria
{
    bool operator () (
        InputDataType const &var1,
        InputDataType const &var2) const;
};

The code that actually uses these different sorting criterias looks like this:

std::shared_ptr<VirtualSortingCriteria> cmpFunctor;
switch(userSelectedSortingCriteria)
{
    case CRITERIA_A:
        cmpFunctor.reset(new RealFunctorVersionA);
        break;
    case CRITERIA_B:
        cmpFunctor.reset(new RealFunctorVersionB);
        break;
    default:
        break;
}

BaseSortingCriteria baseCmpFunctor(cmpFunctor);
mainFunction(inputData, outputData, baseCmpFunctor);

All of this works fine. However, I think it's too complicated to achieve what I want, plus I have the feeling that having to use the polymorphism feature implies the "real" functors can't be inlined anymore, resulting in a probably (although I haven't measured yet) performance penalty.

I'm not sure this could be fixed in a simpler manner using templates (since the functor selection is done at runtime).

Any suggestions? Or am I just overthinking the issue and this is an acceptable solution? What about performance?

I don't use plain C-style functions (bool (*)(InputDataType const &var1, InputDataType const &var2)) in the mainFunction() interface to be able to access the extra functionality provided by functors, such as state, construction parameters, etc.

Thanks a lot in advance for your advice.

Instead of polymorphism and rolling your own type erasure, you could use std::function . For example:

struct RealFunctorVersionA
{
    bool operator () (const InputDataType& var1, const InputDataType& var2) const;
};

struct RealFunctorVersionB
{
    bool operator () (const InputDataType& var1, const InputDataType& var2) const;
};

using MyMapType = std::map<
    InputDataType,
    ValueType,
    const std::function<bool(const InputDataType&, const InputDataType&)>
>;

MyMapType map_a{RealFunctorVersionA{}}
MyMapType map_b{RealFunctorVersionB{}}

Live Demo

Now you don't need your base class. Instead std::function takes care of type-erasing and storing (a copy of) the fuctor object you use to construct your map's comparator.

Also note that I've marked the comparator const so that it can't be changed after the map is constructed. Doing so could easily lead to undefined behavior.


As for performance, your function calls can and will never be inlied if you use a runtime dispatch method. Function call inlining is a compile-time process, so it simply can't be done when you have a potentially unbounded set of potential calls at compile time. Always be wary of premature optimization though. Never make performance decisions without benchmarking.

That being said, std::function will likely be very similar, performance wise, to what you have now though. std::function and virtual dispatch work via very similar mechanisms.


If you want the compiler to potentially inline calls to your comparator, you'll need to make it statically callable. You could use some state to make the calls have different behavior though. For example:

struct MyComparator
{
    bool reverse;
    bool operator () (const InputDataType& var1, const InputDataType& var2) const
    {
        // ...
        if (reverse) {
            return var1 > var2;
        } else {
            return var1 < var2;
        }
    }
};

using MyMapType = std::map<
    InputDataType,
    ValueType,
    const MyComparator
>;

MyMapType map_a{MyComparator{false}};
MyMapType map_a{MyComparator{true}};

Live Demo

This is obviously less flexible, but it could give better performance. Again, you would have to benchmark to say for sure if the gain is even measurable, much less perceptible.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM