C ++ std :: set更新很乏味:我无法就地更改元素


71

我发现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);

基本上,迭代器return bySet是a const_iterator,您不能直接更改其值。

有一个更好的方法吗?或者,也许我应该std::set通过创建自己的方法来覆盖(我不知道它是如何工作的。)


3
如果发现使用2条语句已经很乏味,则进行内联函数。
kennytm 2010年

KennyTM击中了头部。执行此操作在性能方面没有任何缺点,因此请立即进行操作!:-P
j_random_hacker,2010年

1
如果你写了一个更新的功能,你可能想用同样的方式建模为Boost.MultiIndex为:boost.org/doc/libs/release/libs/multi_index/doc/tutorial/...
埃米尔·科米尔

1
cplusplus.com是一个糟糕的参考。因为它而发现语言“乏味”的方面似乎很奇怪。
Lightness Races in Orbit

1
我认为这种方法的缺点是在并发处理的情况下采用了读/写锁定。
UchihaItachi '18年

Answers:


77

set返回const_iterators(标准说set<T>::iteratorconst,这set<T>::const_iteratorset<T>::iterator可能实际上是相同类型的-见23.2.4 / 6在n3000.pdf),因为它是一个有序的容器。如果返回的是常规代码iterator,则可以从容器下方更改项目值,从而可能更改顺序。

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


(non-const)的成员std::set不会返回const_iterator,如果您小心的话,可以修改其元素。为什么这个(不正确的)答案获得如此多的投票?我想念什么?
avakar 2010年

1
我的答案是正确的-您的答案是错误的。我更新了对标准的引用。
Terry Mahaffey'2

6
特里,谢谢您的讨论。我已经检查过:缺陷报告确实在1998年提交,但是没有合并到C ++ 03中。它将进入C ++ 0x。因此,就标准的当前字母而言,您的答案是不正确的,而就意图而言,它是正确的。+1。
avakar 2010年

感谢您的评论和与Avakar(或Avatar?)的辩论。他们提供了很多帮助。
菲戈

2
@avakar:元素在技术上是可变的,但不允许对其进行更改。这是标准中的缺陷。
Lightness Races in Orbit

27

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

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

现在,问题在于棘手的情况:当更新实际上修改key了对象的一部分时会发生什么?您的方法行得通,尽管我承认这很乏味。


3
如果它是一些内部/私有类,则可以在非关键成员上添加可变对象,但这仍然是肮脏的技巧!一旦该类向某些用户公开,id便永远不敢对那些本不应该可变的成员使用可变的!那太恶了!
Marti Nito 2015年

14

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

9

更新:尽管到目前为止,以下内容是正确的,但该行为被认为是缺陷,将在即将发布的标准版本中进行更改。真伤心


有几点使您的问题变得令人困惑。

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

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


1
好吧,我在看C ++ 03的23.1.2 [lib.associative.reqmts],表69,它说:“ a.find(k):迭代器;常数a的const_iterator”。
avakar 2010年

1
我没有C ++ 03 pdf手册(需要花钱)。我以为这是在C ++ 03中修复的,但它可能是03发布后的修复。在n3000.pdf中,它的23.2.4 / 6解释了对于关联容器,迭代器是const迭代器(并且可以与const_iterator类型相同)。您正在使用什么编译器?此行为也在VC9中实现(正在跟踪C ++ 03),这就是为什么我认为该行为是C ++ 03错误修复。
特里·马哈菲

1
不要看表,只看一段解释关联容器上“迭代器”的要求。迭代器可以是“ const”,而无需实际命名为“ const_iterator”。
Terry Mahaffey'2

4
它在标准库缺陷报告中:open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#103。它不能与GCC一起编译,后者会提及DR 103,并且将迭代器和const_iterator的typedef定义为同一类型。-针对OP问题的一种建议的解决方法BTW是const_cast和可变成员。
UncleBens

1
@UncleBens,是的,我也找到了DR。令我有些惊讶的是它没有被纳入C ++ 03中,但却被并入了当前的草案中。从我的角度来看,这是一个相当严峻的变化,因为它将破坏正确的代码。
avakar 2010年

8

您可能要使用一个std::map代替。使用Element影响键排序的那一部分,并将所有都Element作为值。会有一些次要的数据重复,但是您将拥有更轻松(可能更快)的更新。


2

我在C ++ 11中遇到了同样的问题,其中确实::std::set<T>::iterator是常量,因此即使我们知道转换不会影响<不变式,也不允许更改其内容。您可以通过将其包装::std::setmutable_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%的情况下都可以使用,而用户甚至没有注意到集合和实际类型之间存在某些差异。


有趣的主意,我将尝试记住这一点。
马克·兰瑟姆

2
@MarkRansom:是的,但是需要特别注意的是,只有在保证对迭代器后面存储的数据的修改不会改变集合的顺序的情况下,才可以使用此方法。否则,这是UB,它打破!(只是重申一下,以确保没有人朝自己的脚开枪。我并不是在暗示你,尤其是不要意识到这一点。)
bitmask

+1如果您希望基于某个键(该键是合适的容器)进行唯一排序的项目,但又希望保留一些可以更改的“元数据”,则这是理想选择
stijn 2015年

这种方法对我的对象不起作用。当我尝试调用类方法时,特别是在类似于指针的运算符“ 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则可以具有更好的性能。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.