【问题标题】:Efficient substitute for std::map::insert_or_assign with hint带提示的 std::map::insert_or_assign 的有效替代品
【发布时间】:2021-03-09 23:21:23
【问题描述】:

我正在尝试为不支持 C++17 的构建环境编写一个带有 hint 参数的 std::map::insert_or_assign 的替代品。

我希望这个替代品同样高效,并且不需要映射类型为 DefaultConstructible。后一个要求排除了map[key] = value

我想出了这个:

template <class M, class K, class T>
typename M::iterator insert_or_assign(M& map, typename M::const_iterator hint,
                                      K&& key, T&& value)
{
    using std::forward;
    auto old_size = map.size();
    auto iter = map.emplace_hint(hint, forward<K>(key), forward<T>(value));

    // If the map didn't grow, the key already already existed and we can directly
    // assign its associated value.
    if (map.size() == old_size)
        iter->second = std::forward<T>(value);
    return iter;
}

但是,我不知道我是否可以信任 std::map 在密钥已经存在的情况下不会移动分配值两次。这安全吗?如果没有,是否有一种安全的方法可以有效地实现 std::map::insert_or_assign 的替代品,采用 hint 参数?

【问题讨论】:

  • 不是规范,而是cppreference:即使容器中已经有一个带key的元素,也可以构造该元素,这种情况下新构造的元素会被立即销毁。 i> en.cppreference.com/w/cpp/container/map/emplace
  • emplace_hint 中的文字不一样,但我相信 bot 函数也有同样的东西。
  • @NathanOliver 谢谢!我想这排除了我提议的替代品。我没想过要阅读 emplace 的文档来查看是否可以“意外”移动该值。
  • 如果我记得,大多数地图实现不完全忽略提示吗?
  • @MooingDuck,我希望不会,因为在为我的用例插入条目时,它们可能已经被排序。我可以手动检查提示,但它会复制不忽略提示的实现已经完成的工作。

标签: c++ dictionary c++-standard-library


【解决方案1】:

根据 NathanOliver 的评论,他将 cppreference documentation 引用为 std::map::emplace

即使已经有一个元素,也可以构造该元素 使用容器中的密钥,在这种情况下,新构建的 元素将被立即销毁。

如果我们假设 std::map::emplace_hint 也是如此,那么在我在问题中提出的解决方案中,该值可能会过早地消失。

我想出了另一个解决方案(未测试),它只有forwards 的值一次。我承认它不漂亮。 :-)

// Take 'hint' as a mutating iterator to avoid an O(N) conversion.
template <class M, class K, class T>
typename M::iterator insert_or_assign(M& map, typename M::iterator hint,
                                      K&& key, T&& value)
{
    using std::forward;

#ifdef __cpp_lib_map_try_emplace
    return map.insert_or_assign(hint, forward<K>(key), forward<T>(value);
#else
    // Check if the given key goes between `hint` and the entry just before
    // hint. If not, check if the given key matches the entry just before hint.
    if (hint != map.begin())
    {
        auto previous = hint;
        --previous; // O(1)
        auto comp = map.key_comp();
        if (comp(previous->first, key)) // key follows previous
        {
            if (comp(key, hint->first)) // key precedes hint
            {
                // Should be O(1)
                return map.emplace_hint(hint, forward<K>(key),
                                        forward<T>(value));
            }

        }
        else if (!comp(key, previous->first)) // key equals previous
        {
            previous->second = forward<T>(value); // O(1)
            return previous;
        }
    }

    // If this is reached, then the hint has failed.
    // Check if key already exists. If so, assign its associated value.
    // If not, emplace the new key-value pair.
    auto iter = map.find(key); // O(log(N))
    if (iter != map.end())
        iter->second = forward<T>(value);
    else
        iter = map.emplace(forward<K>(key), forward<T>(value)); // O(log(N))
    return iter;
#endif
}

我希望其他人能提出更好的解决方案!

请注意,在诉诸这种丑陋的混乱之前,我会检查 __cpp_lib_map_try_emplace feature test macro 以测试是否支持 std::map::insert_or_assign


编辑:消除了在尝试检查密钥是否已存在于 hint 时的缓慢迭代器算术愚蠢。


编辑 2hint 现在被视为变异迭代器,以避免在其他情况下作为 const_iterator 传递时进行昂贵的 O(N) 转换。这允许我手动检查提示并在提示成功时执行 O(1) 插入或赋值。

【讨论】:

  • @MooingDuck D'oh!你当然是对的。我忘了std::map 是基于节点的。傻我。
  • 删除了慢迭代器算术的愚蠢。
  • 在key不存在且提示成功的情况下,编辑后的解仍然是O(logN)。 :-(
  • 您可以检查hint 是否 节点,在这种情况下跳过find。只是不要增加/减少它。
  • @MooingDuck,我仍然需要将提示转换为变异迭代器,您指出它是 O(N)。我可以做的是将 hint 参数作为常规迭代器而不是 const_iterator。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多