std::unordered_map-使用Lambda专门化KeyEqual

std::unordered_map - specializing KeyEqual with Lambda

本文关键字:Lambda 专门化 KeyEqual 使用 map- unordered std      更新时间:2023-10-16

我想知道是否可以创建一个std::unordered_map类型的变量,如下所示:

std::unordered_map<std::weak_ptr<A>, B, std::hash<std::weak_ptr<A>,
[](const std::weak_ptr<A>& lhs, const std::weak_ptr<B>& rhs) -> bool { return lhs.lock() == rhs.lock(); }>

我希望它能工作,因为KeyEqual模板只需要一个实现()运算符的类型,但visualstudio不允许我编译它,说它缺少lambda所在的类型。

首先,正如Richard Hodges在他的回答中所说,不能使用std::weak_ptr作为密钥,因为它不稳定。忽略这一点,我们可以看看一般的问题:我们可以使用lambdas作为模板参数吗。

通用解决方案是按照以下答案中的描述进行操作。需要注意两件事

  1. lambda必须单独定义
  2. lambda必须作为参数传入

2)的原因是从编译器为lambda创建的类型中删除了默认构造函数。

对于std::set来说,这还不错,但考虑一下std::unordered_map,它没有采用单个密钥比较函数的构造函数:

auto compare = [](const A & lhs, const A & rhs) { return lhs==rhs; };
std::unordered_map<
    A,
    B,
    std::hash<A>,
    decltype(compare)
> map1{0, std::hash<A>{}, compare};

第一个参数是初始bucket大小,并且是由实现定义的。我使用0,假设在插入第一个项时实现将找到一个优化的值。第二个是散列函数,最后是lambda。

我们可以通过用function<bool(A,A)>替换decltype(...)来稍微改进它。这允许我们在头中声明类型,从而将其传递给其他函数,而不需要共享实际的lambda。声明将变为:

typedef std::unordered_map<
            A,
            B,
            std::hash<A>, 
            std::function<bool(A,A)>
    > custom_unordered_map;

它可以如下使用:

custom_unordered_map map2{0, std::hash<A>{}, 
    [](const A & lhs, const A & rhs) { return lhs==rhs; } };

此解决方案将允许直接使用自定义lambda,也可以使用免费函数。

此解决方案的主要好处是,它允许使用不同的比较函数,但使用起来非常冗长。

如果只需要一个比较函数,(对于该类型的用户)一个不太详细的解决方案是定义一个函数:

struct Compare {
    bool operator () (const A & lhs, const A & rhs) {
        return lhs==rhs;
    }
};

然后可以以正常方式使用:

std::unordered_map<A, B, std::hash<A>, Compare> map4;

注意:使用此解决方案,可以使用默认构造函数。

以下是一个完整的示例:

#include <functional>
#include <memory>
#include <unordered_map>
using A = int;
using B = int;

struct Compare {
    bool operator () (const A & lhs, const A & rhs) {
        return lhs==rhs;
    }
};
bool compare_func(const A & lhs, const A & rhs) {
    return lhs==rhs;
}
int main() {
    // Using lamda: default constructor is deleted, so the lambda 
    // must be passed as argument to the constructor.
    auto compare = [](const A & lhs, const A & rhs) { return lhs==rhs; };
    std::unordered_map<
        A,
        B,
        std::hash<A>,
        decltype(compare)
    > map1{0, std::hash<A>{}, compare};
    // Alternative: use std::function. More general, and allows any lambda to be used 
    typedef std::unordered_map<
                A,
                B,
                std::hash<A>, 
                std::function<bool(A,A)>
        > custom_unordered_map;
    custom_unordered_map map2{0, std::hash<A>{}, 
        [](const A & lhs, const A & rhs) { return lhs==rhs; } };
    custom_unordered_map map3{0, std::hash<A>{}, compare_func};
    // Use of function class
    std::unordered_map<A, B, std::hash<A>, Compare> map4;
}

这可以在Ubuntu 15.10上使用命令g++ map_lambda.cpp --std=c++11 -o map_lambda进行编译。

我个人的意见是使用最后一种解决方案,除非需要使用不同的函数进行密钥比较。

您必须在构造函数中定义lambda实现。这里有如何做到这一点的例子。

auto hash = std::unordered_map<std::string, int, std::hash<std::string>, std::function<bool(const std::string&, const std::string&) >>(0, std::hash<std::string>(),
    [](const std::string& lhs, const std::string& rhs)
    {
        return lhs == rhs;
    }
);

这是答案,恐怕你不会喜欢的。

如果没有逻辑错误,你想做的事情是不可能的。

(此时请阅读代码下方的文本。这很重要!)

如果它能起作用,它会是这样的。。。

#include <unordered_map>
#include <string>
#include <cstdint>
#include <utility>
#include <cassert>
struct A {};
struct B{};
int main()
{
    auto owner_equal = [](const auto& l, const auto& r)
    {
        return not l.owner_before(r)
        and not r.owner_before(l);
    };

    using equal_type = decltype(owner_equal);
    std::unordered_map<
    std::weak_ptr<A>,
    B,
    std::hash<std::weak_ptr<A>>,
    equal_type> my_map;
    auto pa = std::make_shared<A>;
    my_map.emplace(pa, B{});
    auto wa = std::weak_ptr<A>(pa);
    pa.reset();
    assert(my_map.find(wa) != my_map.end());
}

那么问题出在哪里呢?

你知道我是如何比较weak_ptr的相等性的吗?这是唯一安全的方法来知道他们是否指的是同一个对象。您必须比较控制块的地址。这是因为,一旦插入到地图中,最后一个shared_ptr可能会消失。

下次在映射的密钥中锁定weak_ptr以获取对象地址时,您将获得nullptr。因此密钥是不稳定的,这是对unordered_map的要求。

以及?

计算哈希也有同样的问题,但更糟。因为您无法访问控制块的地址,只能访问它相对于其他块的相对顺序。

因此,您无法可靠地计算weak_ptr的哈希,因为当您调用lock()时,后续两次调用的答案可能不同。

真的没有解决方案吗?

绝对不是。lock()解决方案在测试中似乎可以工作,但当您的代码进入生产时,它会随机地莫名其妙地失败。

然后呢?

您必须使用std::map并接受O(logN)查找性能。