为什么我们不能使用带有自定义比较 lambda 的 std::multiset 作为 std::map 的值?

Why can not we use `std::multiset` with custom compare lambda as the value of a `std::map`?

这是被问的后续问题

我试过用下面的方式解决

基本

可以向 class 成员的 std::multiset 提供自定义比较 lambda 函数(因为 ),如下所示:

#include <iostream>
#include <set>

const auto compare = [](int lhs, int rhs) noexcept { return lhs > rhs; };
struct Test
{
    std::multiset<int, decltype(compare)> _set{compare};
    Test() = default;
};

够简单了。

我的情况

Testclass的成员是

std::map<std::string, std::multiset<int, /* custom compare */>> scripts{};

我尝试将 std::multiset 与自定义

一起使用

前两个选项成功。但是 lambda 作为自定义比较函数的情况它不起作用。这是 MCVC:https://godbolt.org/z/mSHi1p

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

const auto compare = [](int lhs, int rhs) noexcept { return lhs > rhs; };
class Test
{
private:
    struct Compare
    {
        bool operator()(const int lhs, const int rhs) const noexcept { return lhs > rhs; }
    };

private:
    // std::multiset<int, Compare> dummy;                      // works fine
    // std::multiset<int, std::greater<>> dummy;               // works fine
    std::multiset<int, decltype(compare)> dummy{ compare };    // does not work
    using CustomMultiList = decltype(dummy);

public: 
    std::map<std::string, CustomMultiList> 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 << '\n';
    }
}

错误信息:

error C2280 : '<lambda_778ad726092eb2ad4bce2e3abb93017f>::<lambda_778ad726092eb2ad4bce2e3abb93017f>(void)' : attempting to reference a deleted function
note: see declaration of '<lambda_778ad726092eb2ad4bce2e3abb93017f>::<lambda_778ad726092eb2ad4bce2e3abb93017f>'
note: '<lambda_778ad726092eb2ad4bce2e3abb93017f>::<lambda_778ad726092eb2ad4bce2e3abb93017f>(void)' : function was explicitly deleted
note: while compiling class template member function 'std::multiset<int,const <lambda_778ad726092eb2ad4bce2e3abb93017f>,std::allocator<int>>::multiset(void)'
note: see reference to function template instantiation 'std::multiset<int,const <lambda_778ad726092eb2ad4bce2e3abb93017f>,std::allocator<int>>::multiset(void)' being compiled
note: see reference to class template instantiation 'std::multiset<int,const <lambda_778ad726092eb2ad4bce2e3abb93017f>,std::allocator<int>>' being compiled

听起来我试图默认构造传递的 lambda,which is not possible until

如果是这样的话它发生在哪里是否可以在 的范围内使用 lambda 比较函数 来解决这个问题?

要在一行中完成,您需要这样的东西:

t.scripts.try_emplace("Linux", compare).first->second.insert(5);

这是因为 lambda compare 必须传递给 multiset 的构造函数。否则没有比较对象,无法构造multiset

演示:https://godbolt.org/z/rVb3-D

It sounds like I tried to default construct the passed lambda, which is not possible until . If that the case where has it happened?

。这正是由于 std::map::operator[]

行的调用而发生的事情
t.scripts["Linux"].insert(5);
//       ^^^^^^^^^

让我们详细了解一下。上面的调用将导致调用以下重载,因为密钥是临时 std::stringconst char*.

构造的
T& operator[]( Key&& key );

因为 C++17 this is equivalent to:

return this->try_emplace(
    std::move(key)).first  ->  second;
//               key_type    mapped_type
//               ^^^^^^^^    ^^^^^^^^^^^
//                  |           |
//                  |           |
//             (std::string)  (std::multiset<int, decltype(compare)>)
//                  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                  |           |                               (default-construction meaning)
//                  |       default-construction -->   std::multiset<int, decltype(compare)>{}
//               move-construction                                                          ^^

其中 key_type(即从 const char* 临时构建的 std::string)应该是 move constructible , 碰巧很好。

mapped_type(即std::multiset<int, decltype(compare)>)应该是default construct ed first and that requires the compare lambda should be also default constructed. From cppreference.com:

ClosureType::ClosureType()

ClosureType() = delete;   (until C++14)
ClosureType() = default;  (since C++20)(only if no captures are specified)

Closure types are not DefaultConstructible. Closure types have a deleted (until C++14)no (since C++14) default constructor. (until C++20)


If no captures are specified, the closure type has a defaulted default constructor. Otherwise, it has no default constructor (this includes the case when there is a capture-default, even if it does not actually capture anything). (since C++20)

这意味着,lambda 闭包类型的默认构造在 C++17 中不可用(这就是编译器错误所抱怨的)。

另一方面,在 compare lambda 中没有指定 捕获(即无状态 lambda),因此编译器可以显式默认它支持C++20标准。


Is it possible to solve this using a lambda compare function within the scope of till ?

不是通过使用std::map::operator[](正如上面解释的原因),而是,@JohnZwinck的方式在他的回答中提到。我想解释一下,它是如何工作的。

std::multiset1构造函数之一提供了传递比较器对象的可能性。

template< class InputIt >
multiset( InputIt first, InputIt last,
          const Compare& comp = Compare(),
//        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
          const Allocator& alloc = Allocator() );

同时,lambda闭包类型的复制构造函数和移动构造函数已经defaulted since C++14。这意味着,如果我们有可能提供 lambda 作为第一个参数2(通过复制或移动它),它将是Basic 案例,问题中显示的内容。

std::multiset<int, decltype(compare)> dummy{ compare };            // copying
std::multiset<int, decltype(compare)> dummy{ std::move(compare) }; // moving

幸运的是,C++17引入了成员函数std::map::try_emplace

template <class... Args>
pair<iterator, bool> try_emplace(key_type&& k, Args&&... args);

通过它可以将 lambda 作为第一个传递给 std::multiset 的 above-mentioned 构造函数1 argument2 如上所示。如果我们将其转换为 Test class 的成员函数,则可以将元素插入到 scripts 映射的 CustomMultiList(即值)中。

解决方案看起来像(与链接 post 相同,因为我在问这个问题后写了那个答案!)

(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[]
    // latest version of GCC has already included this change
    //t.scripts["Linux"].insert(5);
    //t.scripts["Linux"].insert(8);
    //t.scripts["Linux"].insert(0);

    return 0;
}