【问题标题】:std::unordered_map - specializing KeyEqual with Lambdastd::unordered_map - 使用 Lambda 专门化 KeyEqual
【发布时间】:2016-04-03 14:03:24
【问题描述】:

我想知道是否可以像这样创建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 模板只需要一个实现 () 运算符的类型,但 Visual Studio 不会让我编译它,说它缺少 lambda 所在的类型。

【问题讨论】:

  • 试试decltype([](...) { ... })
  • @kukyakya 这不是合法的 C++,即使它可以编译。

标签: c++ c++11 visual-c++ lambda


【解决方案1】:

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

通用解决方案是按照the following answer 中的说明进行操作。有两点需要注意

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

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

我们可以通过将decltype(...) 替换为function&lt;bool(A,A)&gt; 来稍微改进它。这允许我们在头文件中声明类型,从而将其传递给其他函数,而无需共享实际的 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 编译。

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

【讨论】:

  • 谢谢,这个答案是最精致的,所以我接受你的。我试图解决比较器结构解决方案,但是现在我明白为什么它更明智了。你有关于weak_ptr 散列问题的提示吗?
  • 我的第一个解决方案是使用在指针指向的对象中找到的唯一键。另一种选择是使用shared_ptr。在这两种情况下,除非您定期清理,否则地图的大小会无限增长。基本上它取决于A 的类型。如果您需要更好的答案,我建议您添加一个新问题。
【解决方案2】:

您必须在构造函数中定义 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;
    }
);

【讨论】:

    【解决方案3】:

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

    如果没有逻辑错误,您要尝试做的事情是不可能的。

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

    如果它可以工作,它会是这样的......

    #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。所以key是不稳定的,这是unordered_map的要求。

    和?

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

    所以你不能可靠地计算一个weak_ptr的哈希,因为当你调用lock()时,两个后续调用的答案可能不同。

    真的没有办法吗?

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

    然后呢?

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

    【讨论】:

    • decltype(owner_equal) 不是无序地图的有效类型,除非自从我上次戳它后标准发生了变化?
    • @Yakk 你看过我的评论了吗?我觉得这是答案的重要部分。
    • 非常有趣...当谈到智能指针理论时,我还有些不安全,所以感谢您指出这个问题。但是,std::map 将如何解决这个问题?键需要是可排序的,你如何为 std::weak_ptr 定义一个合理的排序?
    • @xaser 这就是 owner_less 模板的用途。它比较控制块的地址,允许对弱指针进行严格的弱排序。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-05
    • 2017-10-09
    相关资源
    最近更新 更多