简体   繁体   中英

How to provide custom comparator for `std::multiset` without overloading `operator()`, `std::less`, `std::greater`?

I want a custom comparator for the following code. However, I am not allowed to overload operator() , std::less , std::greater .

I tried to achieve this using lambda but gcc won't allow me to use auto as a non-static member. Any other way to make this work?

#include <iostream>
#include <map>
#include <set>

class Test 
{
public:
    // bool operator () (const int lhs, const int rhs) { // not allowed
    //     return lhs > rhs;
    // };    
    using list = std::multiset<int  /*, Test*/>;
    std::map<const char*, list> scripts;
};

int main() 
{
    Test t;
    t.scripts["Linux"].insert(5);
    t.scripts["Linux"].insert(8);
    t.scripts["Linux"].insert(0);

    for (auto a : t.scripts["Linux"]) {
        std::cout << a << std::endl;
    }

    std::cout << "end";
}

Edit : With lambdas

class Test 
{
  public:
    auto compare = [] (const int a, const int b) { return a < b;}
    using list = std::multiset<int, compare>;    //here
    std::map<const char*, list> scripts;
};

Error:

'auto' not allowed in non-static class member
 auto compare = [] (const int a, const int b) { return a < b;}

I want a custom comparator for the following code. However, I cannot overload operator() , std::less , std::greater .

I assume that you are not allowed to overload operator() of the Test class, but could be that of other class. If so, create a internal private functor which overloads operator() and that could be part of the alias using list = std::multiset<int, Compare>;

class Test
{
private:
    struct Compare
    {
        bool operator()(const int lhs, const int rhs) const /* noexcept */ { return lhs > rhs; }
    };

public:
    using list = std::multiset<int, Compare>;
    std::map<std::string, list> scripts;
};

I tried to achieve these using lambdas but gcc won't allow me to use auto as a non-static member. Any other way to make this work?

Update : After researching a while, I found a way to go which does work with a lambda function.

The idea is to use the decltype of std::multiset with custom lambda compare as the key of the std::map scripts . In addition to that, provide a wrapper method for inserting the entries to the CustomMultiList .

Complete example code: ( See live )

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

// provide a lambda compare
const auto compare = [](int lhs, int rhs) noexcept { return lhs > rhs; };

class Test
{
private:
    // make a std::multi set with custom compare function  
    std::multiset<int, decltype(compare)> dummy{ compare };
    using CustomMultiList = decltype(dummy); // use the type for values of the map 
public:
    std::map<std::string, CustomMultiList> scripts{};
    // warper method to insert the `std::multilist` entries to the corresponding keys
    void emplace(const std::string& key, const int listEntry)
    {
        scripts.try_emplace(key, compare).first->second.emplace(listEntry);
    }
    // getter function for custom `std::multilist`
    const CustomMultiList& getValueOf(const std::string& key) const noexcept
    {
        static CustomMultiList defaultEmptyList{ compare };
        const auto iter = scripts.find(key);
        return iter != scripts.cend() ? iter->second : defaultEmptyList;
    }
};


int main()
{
    Test t{};
    // 1: insert using using wrapper emplace method
    t.emplace(std::string{ "Linux" }, 5);
    t.emplace(std::string{ "Linux" }, 8);
    t.emplace(std::string{ "Linux" }, 0);


    for (const auto a : t.getValueOf(std::string{ "Linux" }))
    {
        std::cout << a << '\n';
    }
    // 2: insert the `CustomMultiList` directly using `std::map::emplace`
    std::multiset<int, decltype(compare)> valueSet{ compare };
    valueSet.insert(1);
    valueSet.insert(8);
    valueSet.insert(5);
    t.scripts.emplace(std::string{ "key2" }, valueSet);

    // 3: since C++20 : use with std::map::operator[]
    t.scripts["Linux"].insert(5);
    t.scripts["Linux"].insert(8);
    t.scripts["Linux"].insert(0);

    return 0;
}

Until lambda are not default constructable and copyable . But, the std::map::operator[] does requered the mapped_type to be copy constructible and default constructible . Hence the insertion to the value of the scripts map(ie to std::multiset<int, decltype(/*lambda compare*/)> ) using subscription operator of std::map is only possible from from C++20.

You can use a function pointer of a comparison function in the constructor:

main.cpp

#include <iostream>
#include <set>

using compType=bool(*)(int lhs, int rhs);

bool custom_compare_function(int lhs, int rhs)
{
    return lhs>rhs;
}


using list = std::multiset<int,compType>;
int main() {
    list l(&custom_compare_function);
    l.insert(1);
    l.insert(4);
    l.insert(2);
    for (auto& item: l) std::cout<<item<<std::endl;
}

produces the output

$ g++ main.cpp 
$ ./a.out 
4
2
1

There is a problem with your approach even if you could define a lambda the way you want to. Take a look at the multiset declaration :

template<
    class Key,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<Key>
> class multiset;

Notice how each template parameter is a type (using the class keyword). Now look at how you tried to define your list:

using list = std::multiset<int, compare>;
                            ^      ^
                          type   value

The first parameter is good, but the second is a mismatch. The Compare parameter needs to be a type, not an object. One general way to resolve this sort of situation is to replace compare with decltype(compare) , but that seems to be not what you want (plus it is problematic for lambda types). You seem to want a default constructed list to use compare instead of merely a default constructed object of the same type.

So what you need is a class whose default constructed object implements operator() in a way that gives the order you want. Since we're dealing with int , the standard library has some ready-made types for this purpose, namely std::less and std::greater .

using list = std::multiset<int, std::greater<int>>;

However, I cannot overload operator(), std::less, std::greater.

Hmm... this suggests that the example code may have been over-simplified, as an overload is not called for. OK, let's suppose the list is of some type that's harder to deal with, say:

class I { /* Internals not important for this example. */ };
using list = std::multiset<I, ???>;

If you are allowed to modify I , then the simplest approach might be to define operator> (or operator< ) for objects of type I . Since std::greater (or std::less ) uses this operator, you get the desired order from the standard template without overloading it.

If you are not allowed to modify I , then I think you're left with writing your own function object , as this is one situation where a lambda is inadequate. Fortunately, classes implementing function objects are easy to write; lambdas have superseded them in other situations mostly because the lambda syntax tends to be more convenient.

struct CompareI {
    bool operator() (const I & lhs, const I & rhs) const { return /* fill this in */; }
};
using list = std::multiset<I, CompareI>;

While this defines an operator() , it is not an overload. So it should satisfy the requirements given to you.

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