【问题标题】:Modifying the value of a user defined type in `std::set`在 `std::set` 中修改用户定义类型的值
【发布时间】:2020-02-10 17:26:52
【问题描述】:

考虑我有一个用户定义类型的情况,其中有一个 id() 成员函数,它返回一个唯一的 std::string

我想要一个包含这些对象的容器,其中 id() 唯一标识元素,但我想“使用”这些对象来做其他可能修改其成员的事情。

我目前正在构造对象。通过调用std::set::emplace 并捕获返回的迭代器,布尔对。

但是我不能修改它的值,因为迭代器是 const。

有什么好方法可以做我想做的事吗?我能想到的只有两个:

  1. unique_ptrs 存储到set 中的对象中,这样区分它的是指针值而不是名称,并且可以修改指向的对象。
  2. 使用id() 作为密钥存储map,但这意味着我复制了密钥。

如果它们有合适的容器来解决我的问题,我很乐意使用被广泛采用的现代库,例如 boost。

【问题讨论】:

  • 您是否使用set 来确保容器中只有唯一的对象?
  • @NathanOliver 是的,并且还提供基于密钥的 sub O(n) 查找时间。我对std::unordered_set 也有同样的问题
  • 如果您注意不影响严格的弱排序,您可能能够摆脱可变类成员和 const 类方法。这将是相当丑陋的,但它会工作。 unordered_set 也是如此,只要哈希值不受影响。
  • @SamVarshavchik 这绝对不理想,因为我使用 const 来影响类型的真正目的。我认为指针路线将是我最好的选择。
  • 您是否考虑过使用.extract 成员函数从集合中删除所需的项目,对其进行修改,然后将其放回?这避免了与“正常”插入相关的所有分配。

标签: c++ stl c++17 c++20 stdset


【解决方案1】:

有什么好方法可以做我想做的事吗?

不,不是。 std::set 的粒度是对象级别的。无法表示对象的一部分对键有贡献。

有些人建议声明所有非关键成员mutable。这是错误的,因为mutable 用于隐藏在对象公共接口之外的事物(例如互斥体)。

“官方”方法是将对象从集合中取出,对其进行修改并重新放入。C++17 有set::extract,这有助于稍微提高此任务的性能(如果你永远不会修改密钥,因为树仍然需要检查/重新平衡)。

我想“使用”这些对象来做其他可能会修改其成员的事情。

如果你绝对确定你永远不会修改对象键,那就抛弃constness。从法律的角度来看it is OK 抛弃不是天生的对象的 constness const。为了更加安全,您可以将密钥包装到另一个 const 成员中:

struct Element {
    const Key key;
    Value value;
};

如果您的数据多维数据集包含多个集合,每个集合在键上使用自己的“视图”,这将无济于事。

1.将unique_ptrs 存储到set 中的对象中

由于额外的间接性,这将是一种悲观。由于元素位于堆上,因此您将需要额外的缓存未命中。如果您不小心修改了密钥,则会再次以 UB 结尾。

2.使用id() 作为密钥存储map

是的,这种方法的不同变体是可能的,但您仍然必须确保永远不要修改密钥。

例如,您可以存储一个键 + 指向数据的指针。这种方法通常与带有线性探测的dense_hash_set 结合使用,以获得最佳性能。由于在找到元素后该值仅被访问一次,因此它位于其他位置并不重要。

【讨论】:

  • 重新插入 extracted 节点,提示应该是 O(1),当然你需要保留下一个迭代器:auto nh = c.extract(it++); nh.value()./* ... */; it = c.insert(it, std::move(nh));
  • 是的,但是 O(0)(即不这样做)优于 O(1)。
【解决方案2】:

我建议将Boost.MultiIndex 用作drop-in replacementstd::set,因为它添加了允许修改元素的modify 方法,检查容器内的位置是否已更改:

#include <boost/multi_index_container.hpp>
#include <boost/multi_index/ordered_index.hpp>

struct S { /* ... */ };
boost::multi_index_container<S> t; // default configuration emulates std::set<S>
auto [it, inserted] = t.emplace(...);
t.modify(it, [&](S& s) {
    // modify s here
    // if the key is unchanged, s does not move
    // the iterator `it` remains valid regardless
});

Example.

在检查密钥是否确实未更改时会产生少量开销,但与程序的其余部分相比,这应该是最小的,并且应该可以很好地优化和预测。

【讨论】:

    【解决方案3】:

    std::set 保持其元素排序,并且元素排序的 对应于元素本身。因此,std::set 中的元素是const 限定的,以防止用户修改元素(即键)并因此破坏std::set 顺序。

    传统上,如果您想修改std::set 的元素,您必须首先从std::set 中删除您要修改的元素对象,对其进行修改,然后再次将其插入std::set。问题是这会导致分配一个std::set 内部节点。

    从 C++17 开始,您可以在 std::set 中删除和重新插入元素,而无需分配 std::set 内部节点,这要归功于 std::set::extract()。该成员函数返回与请求的元素对应的节点句柄。通过这个返回的节点修改元素后,可以重新插入对应insert()重载的节点。由于您正在重用已分配的节点,因此不会进行节点分配。

    这些方法的缺点 - 无论是否发生分配 - 将元素重新插入std::set 需要对集合大小的对数时间(除非您可以利用 提示 em> 到insert())。

    抛弃常量并修改std::set元素

    只要您的std::set 的比较函数不考虑您更改的数据成员,您仍然可以从std::set 的元素中转换const 并修改其数据成员。也就是说,如果您只修改属于比较函数不考虑的std::set 的元素的数据成员,则顺序不会中断。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-09-06
      • 2016-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-01
      相关资源
      最近更新 更多