C++ std::set 更新很麻烦:我不能就地更改元素。

80

我发现在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 来覆盖它(但我不知道它的工作原理..)


3
如果你发现使用两条语句已经很繁琐,可以创建一个内联函数。 - kennytm
KennyTM 一针见血。在性能方面没有任何缺点,所以赶快做吧!:-P - j_random_hacker
1
如果您编写更新函数,可能希望将其建模方式与Boost.MultiIndex相同:http://www.boost.org/doc/libs/release/libs/multi_index/doc/tutorial/basics.html#ord_updating - Emile Cormier
1
cplusplus.com是一个糟糕的参考资料。因为它而觉得语言某些方面“乏味”似乎有点奇怪。 - Lightness Races in Orbit
1
我认为这种方法的缺点是在并发处理的情况下需要进行读/写锁定。 - UchihaItachi
1
如果您向insert提供一个提示迭代器,那么这将更有效率。最简单的方法似乎是set.insert(set.erase(oldItem),newItem) - Tim Sylvester
7个回答

82

set返回const_iterators(标准规定set<T>::iteratorconst,并且set<T>::const_iteratorset<T>::iterator实际上可能是相同的类型 - 见n3000.pdf中的23.2.4 / 6),因为它是一个有序容器。如果它返回一个常规的iterator,则允许您从容器下更改元素的值,这可能会改变排序。

您的解决方案是修改set中项目的惯用方法。


(non-const) std::set 的成员不返回 const_iterator,如果你小心一点,你 可以 修改它的元素。为什么这个(不正确的)答案得到了这么多赞?我错过了什么吗? - avakar
1
我的答案是正确的 - 你的是错误的。我已经在我的帖子中附上了对标准的参考。 - Terry Mahaffey
7
谢谢你的讨论,Terry。我重新检查了一下:这个缺陷报告确实在1998年提交过,但没有被纳入C++03标准中。它将被纳入C++0x标准中。因此,尽管你的答案与当前标准的字面意思不符,但在意图方面是正确的。+1。 - avakar
2
@avakar:元素在技术上是可变的,但不允许改变它们。这是标准中的缺陷。 - Lightness Races in Orbit
@amine 当然可以。setmap都需要不可变的键,如果您有想要更改的非键字段,则应将其作为map的值部分。(我否认使用set元素中的mutable成员之类的选项:yuk) - underscore_d
显示剩余2条评论

33

C++17引入了extract,请参见巴里的回答


如果您被困在旧版本中,有两种方法可以解决这个问题,在简单情况下:

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

现在,问题来了:当更新实际修改对象的key部分时会发生什么?您的方法虽然可行,但我承认它很繁琐。


4
如果某些成员变量不是关键变量,将可变性(mutable)添加到它们上面可能在一些内部/私有类中没有问题,但这仍然是一种肮脏的黑客行为! 一旦该类暴露给一些用户,我永远不敢对那些本来不应该被改变的成员使用可变性(mutable)! 那太邪恶了! - Marti Nito

25

有了extract(),在 C++17 中你可以更好地处理问题,这要感谢 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));

这样可以避免类型的额外复制和额外的分配/释放,同时也适用于只能移动但不能拷贝的类型。

您还可以按键提取。如果该键不存在,则会返回一个空节点:

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

9

更新:虽然以下内容目前是正确的,但这种行为被认为是一个缺陷,并将在标准的即将发布的版本中进行更改。非常遗憾。


你的问题有几个点让人感到困惑。

  1. 函数可以返回值,类不能。 std::set 是一个类,因此不能返回任何东西。
  2. 如果你能调用s.erase(iter),那么iter不是一个const_iteratorerase需要一个非const迭代器。
  3. 只要集合也是非const的,std::set的所有成员函数返回的迭代器都是非const迭代器。

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

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

如果你的更新改变了元素的顺序,那么你需要先删除再重新插入。

1
好的,我正在查看C++03的23.1.2[lib.associative.reqmts],表格69,并且它说:“a.find(k):迭代器;对于常量a的const_iterator”。 - avakar
1
我手头没有C++03的pdf(需要花钱购买)。我认为这个问题在C++03中已经修复了,但也有可能是后来的修复。在n3000.pdf中,第23.2.4/6节解释了对于关联容器,迭代器是一个const迭代器(并且可以是与const_iterator相同的类型)。你使用的是哪个编译器?这种行为也在VC9中实现了(它正在跟踪C++03),这就是为什么我认为这种行为是C++03的错误修复。 - Terry Mahaffey
1
不要看表格,看段落解释关联容器上“迭代器”的要求。一个迭代器可以是“const”,而不必实际命名为“const_iterator”。 - Terry Mahaffey
4
这段内容在标准库缺陷报告中:http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#103。它无法在GCC中编译,其中提到了DR 103并将迭代器和const_iterator类型都定义为相同的类型。顺便提一下,可以使用const_cast和mutable成员来解决OP的问题。 - UncleBens
1
@UncleBens,是的,我也找到了DR。我有点惊讶它没有被纳入C++03,但却被纳入了当前的草案中。从我的角度来看,这是一个相当严重的变化,因为它将破坏本来正确的代码。 - avakar
显示剩余6条评论

8
您可能需要使用std::map。 使用影响键排序的Element部分作为键,将所有Element作为值。 这里会有一些轻微的数据重复,但您会拥有更容易(可能更快)的更新。

3

我在C++11中遇到了同样的问题,事实上::std::set<T>::iterator是不可更改的,因此即使我们知道转换不会影响<不变式,也不允许更改其内容。您可以通过将::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%的情况下用户甚至不会注意到在设置和实际类型之间有任何东西。


有趣的想法,我会尝试记住这个。 - Mark Ransom
2
@MarkRansom:是的,但为了更加保险,应该注意到,只有在修改迭代器后面存储的数据保证不会改变集合排序时才能使用。否则,这是未定义行为,它一定会出问题!(再次强调这一点,以确保没有人会自己给自己惹麻烦。我并不是在暗示你,特别是你没有意识到这一点。) - bitmask
+1 这是理想的选择,如果您想要基于某些键(对于这种情况,set 是适当的容器)获取唯一排序的项目,但也想保留一些与它们相关的“元数据”,这些元数据可能会发生变化。 - stijn
这种方法在我的对象上不起作用,特别是在我尝试调用类方法时使用指针样的运算符"operator->()"。 - Erman

0

在某些情况下,这更快:

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

如果值通常不在std::set中,则这样可以获得更好的性能。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接