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

此生再无相见时 提交于 2019-12-23 21:27:55

问题


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;}

回答1:


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 c++20 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(i.e. to std::multiset<int, decltype(/*lambda compare*/)>) using subscription operator of std::map is only possible from from C++20.




回答2:


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



回答3:


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.



来源:https://stackoverflow.com/questions/56400801/how-to-provide-custom-comparator-for-stdmultiset-without-overloading-operat

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!