【问题标题】:C++ std::set update is tedious: I can't change an element in placeC++ std::set 更新很乏味:我无法就地更改元素
【发布时间】:2011-01-14 03:26:26
【问题描述】:

我发现std::set 上的更新操作很乏味,因为cppreference 上没有这样的 API。所以我目前做的是这样的:

//find element in set by iterator
Element copy = *iterator;
... // update member value on copy, varies
Set.erase(iterator);
Set.insert(copy);

基本上Set 返回的迭代器是const_iterator,你不能直接改变它的值。

有没有更好的方法来做到这一点?或者也许我应该通过创建自己的来覆盖std::set(我不知道它是如何工作的……)

【问题讨论】:

  • 如果您发现使用 2 条语句已经很乏味,请创建一个内联函数。
  • KennyTM 一针见血。这样做在性能方面没有缺点,所以现在就去做吧! :-P
  • 如果你写了一个更新函数,你可能想用和Boost.MultiIndex一样的方式来建模:boost.org/doc/libs/release/libs/multi_index/doc/tutorial/…
  • cplusplus.com 是一个糟糕的参考。查找语言“乏味”的各个方面,因为它看起来……很奇怪。
  • 我认为这种方法的缺点是在并发处理的情况下采用读/写锁。

标签: c++ stl set


【解决方案1】:

set 返回const_iterators(标准上说set<T>::iteratorconst,而set<T>::const_iteratorset<T>::iterator 实际上可能是同一类型 - 请参阅 n3000.pdf 中的 23.2.4/6)因为它是一个有序的容器。如果它返回一个普通的iterator,你就可以从容器下面更改项目值,可能会改变顺序。

您的解决方案是更改 set 中项目的惯用方式。

【讨论】:

  • (非常量)std::set 的成员不返回 const_iterator,如果您小心的话,您可以修改其元素。为什么这个(不正确的)答案会得到如此多的支持?我错过了什么?
  • 我的答案是正确的——你的答案是错误的。我用对标准的引用更新了我的帖子。
  • 特里,感谢您的讨论。我复查了:缺陷报告确实是在 1998 年提交的,但没有并入 C++03。它将进入 C++0x。因此,尽管就标准的当前字母而言,您的答案不正确,但就意图而言,它是正确的。 +1。
  • 感谢您的评论以及与 Avakar(或 Avatar?)的辩论。他们帮了很多忙。
  • @avakar:元素在技术上是可变的,但不允许更改它们。这是标准中的一个缺陷。
【解决方案2】:

有两种方法可以做到这一点,在简单的情况下:

  • 您可以在不属于键的变量上使用mutable
  • 您可以将班级分成Key Value 对(并使用std::map

现在,问题在于棘手的情况:当更新实际修改对象的key 部分时会发生什么?你的方法行得通,虽然我承认它很乏味。

【讨论】:

  • 在非关键成员上添加 mutable 如果它是一些内部/私有类可能没问题,但它仍然是一个肮脏的黑客!一旦类暴露给一些用户,id 就不敢对不可变的成员使用 mutable!那是邪恶的!
【解决方案3】:

在 C++17 中,您可以使用 extract() 做得更好,感谢 P0083

// remove element from the set, but without needing
// to copy it or deallocate it
auto node = Set.extract(iterator);
// make changes to the value in place
node.value() = 42;
// reinsert it into the set, but again without needing 
// to copy or allocate
Set.insert(std::move(node));

这将避免您的类型的额外副本和额外的分配/解除分配,并且也适用于仅移动类型。

您也可以通过按键extract。如果键不存在,这将返回一个空节点:

auto node = Set.extract(key);
if (node) // alternatively, !node.empty()
{
    node.value() = 42;
    Set.insert(std::move(node));
}

【讨论】:

    【解决方案4】:

    更新:虽然目前以下情况属实,但该行为被视为defect,并将在即将发布的标准版本中进行更改。好难过。


    有几点让您的问题相当混乱。

    1. 函数可以返回值,类不能。 std::set 是一个类,因此不能返回任何内容。
    2. 如果你可以打电话给s.erase(iter),那么iter就不是const_iteratorerase 需要一个非常量迭代器。
    3. std::set 的所有返回迭代器的成员函数都会返回一个非常量迭代器,只要该集合也是非常量的。

    只要更新不改变元素的顺序,您就可以更改集合中元素的值。以下代码可以编译并正常工作。

    #include <set>
    
    int main()
    {
        std::set<int> s;
        s.insert(10);
        s.insert(20);
    
        std::set<int>::iterator iter = s.find(20);
    
        // OK
        *iter = 30;
    
        // error, the following changes the order of elements
        // *iter = 0;
    }
    

    如果您的更新更改了元素的顺序,那么您必须擦除并重新插入。

    【讨论】:

    • 好吧,我正在查看 C++03 的 23.1.2[lib.associative.reqmts],表 69,它说:“a.find(k): iterator; const_iterator for constant一个”。
    • 我手边没有 C++03 pdf(它要花钱)。我认为这是在 C++03 中修复的,但它可能是 03 后的修复。在 n3000.pdf 中,它是 23.2.4/6,它解释了对于关联容器,迭代器是一个 const 迭代器(并且可以与 const_iterator 的类型相同)。你用的是什么编译器?此行为也在 VC9 中实现(跟踪 C++03),这就是为什么我认为该行为是 C++03 错误修复。
    • 别看表格,看段落解释“迭代器”对关联容器的要求。迭代器可以是“const”,但实际上不被命名为“const_iterator”。
    • 它在标准库缺陷报告中:open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#103。它不能与 GCC 一起编译,它提到了 DR 103,并且 typedef 将 iterator 和 const_iterator 都设置为相同的类型。 - 一种针对 OP 问题的建议解决方法,顺便说一句,是 const_cast 和 mutable 成员。
    • @UncleBens,是的,我也找到了 DR。我有点惊讶它没有进入 C++03,而是被合并到当前的草案中。从我的角度来看,这是一个相当严重的变化,因为它会破坏其他正确的代码。
    【解决方案5】:

    您可能想改用std::map。使用Element 中影响key 排序的部分,将Element 全部作为value。会有一些小的数据重复,但更新会更容易(并且可能更快)。

    【讨论】:

      【解决方案6】:

      我在 C++11 中遇到了同样的问题,其中 ::std::set&lt;T&gt;::iterator 确实是常量,因此不允许更改其内容,即使我们知道转换不会影响 &lt; 不变量。您可以通过将 ::std::set 包装成 mutable_set 类型或为内容编写包装器来解决此问题:

        template <typename T>
        struct MutableWrapper {
          mutable T data;
          MutableWrapper(T const& data) : data(data) {}
          MutableWrapper(T&& data) : data(data) {}
          MutableWrapper const& operator=(T const& data) { this->data = data; }
          operator T&() const { return data; }
          T* operator->() const { return &data; }
          friend bool operator<(MutableWrapper const& a, MutableWrapper const& b) {
            return a.data < b.data;
          }   
          friend bool operator==(MutableWrapper const& a, MutableWrapper const& b) {
            return a.data == b.data;
          }   
          friend bool operator!=(MutableWrapper const& a, MutableWrapper const& b) {
            return a.data != b.data;
          }   
        };
      

      我发现这要简单得多,它在 90% 的情况下都有效,用户甚至不会注意到集合和实际类型之间存在某种差异。

      【讨论】:

      • 有趣的想法,我会试着记住这个。
      • @MarkRansom:是的,但为了更加确定,应该注意的是,只有当迭代器后面存储的数据的修改得到保证时才能使用不要改变集合的顺序。否则,这是 UB,它 崩溃! (只是重申这一点,以确保没有人在脚上开枪。我并不是在暗示你,特别是不要意识到这一点。)
      • +1 如果您想要基于某个键的唯一排序项目(对于该集合是适当的容器)但又想保留一些可以更改的“元数据”,这是理想的选择
      • 这种方法不适用于我的对象。当我尝试调用我的类方法时,特别是在类似指针的运算符“operator->()”上。
      【解决方案7】:

      在某些情况下会更快:

      std::pair<std::set<int>::iterator, bool> result = Set.insert(value);
      if (!result.second) {
        Set.erase(result.first);
        Set.insert(value);
      }
      

      如果值通常不在std::set 中,那么这可以有更好的性能。

      【讨论】:

        猜你喜欢
        • 2011-11-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-03-25
        • 1970-01-01
        相关资源
        最近更新 更多