80

I find the update operation on std::set tedious since there's no such an API on cppreference. So what I currently do is something like this:

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

Basically the iterator return by Set is a const_iterator and you can't change its value directly.

Is there a better way to do this? Or maybe I should override std::set by creating my own (which I don't know exactly how it works..)

Dev Null
  • 4,731
  • 1
  • 30
  • 46
Figo
  • 1,700
  • 3
  • 15
  • 24
  • 3
    Make an inline function if you find using 2 statements is already tedious. – kennytm Feb 07 '10 at 19:02
  • KennyTM hit the nail on the head. There are no downsides performancewise to doing this, so just do it already! :-P – j_random_hacker Feb 07 '10 at 19:24
  • 1
    If you write an update function, you may want to model it the same way as Boost.MultiIndex: http://www.boost.org/doc/libs/release/libs/multi_index/doc/tutorial/basics.html#ord_updating – Emile Cormier Feb 07 '10 at 21:55
  • 1
    cplusplus.com is an awful reference. Finding aspects of the language "tedious" because of it seems... odd. – Lightness Races in Orbit Jun 12 '11 at 22:24
  • 1
    I think the downside to this approach is taking a Read/Write Lock in the case of concurrent processing. – UchihaItachi May 31 '18 at 08:59
  • 1
    This is more efficient if you provide a hint iterator to the `insert`. The simplest way to do this seems to be `set.insert(set.erase(oldItem),newItem)`. – Tim Sylvester Jul 20 '21 at 17:40

7 Answers7

82

set returns const_iterators (the standard says set<T>::iterator is const, and that set<T>::const_iterator and set<T>::iterator may in fact be the same type - see 23.2.4/6 in n3000.pdf) because it is an ordered container. If it returned a regular iterator, you'd be allowed to change the items value out from under the container, potentially altering the ordering.

Your solution is the idiomatic way to alter items in a set.

Tas
  • 7,023
  • 3
  • 36
  • 51
Terry Mahaffey
  • 11,775
  • 1
  • 35
  • 44
  • Members of (non-const) `std::set` does not return `const_iterator` and you *can* modify its elements if you're careful. Why is this (incorrect) answer getting so many upvotes? What am I missing? – avakar Feb 07 '10 at 19:36
  • 1
    My answer is correct - yours is wrong. I updated my post with references to the standard. – Terry Mahaffey Feb 07 '10 at 19:43
  • 7
    Terry, thank you for discussion. I've rechecked: the defect report had indeed been submitted in 1998, but was not incorporated into C++03. It will be into C++0x. So while your answer is not correct as far as the current letter of the standard is concerned, it is correct as far as the intent goes. +1. – avakar Feb 07 '10 at 20:02
  • Thanks for your comment and your debate with Avakar (Or Avatar?). They helped alot. – Figo Feb 07 '10 at 20:28
  • 2
    @avakar: The elements are technically mutable but it's not allowed to alter them. This is a defect in the standard. – Lightness Races in Orbit Jun 12 '11 at 22:25
  • So I guess that the proper way to have mutable parts, associated to unique keys, while still using STL, is switching to std::map. That's what I did anyway – amine Jan 17 '18 at 14:46
  • @amine Yes, of course. Both `set` and `map` need immutable keys, and if you have non-key fields you want to change, those should be the value part of a `map`. (I disavow options like using `mutable` members in `set` elements: yuk) – underscore_d Sep 22 '18 at 12:47
33

C++17 introduced extract, see Barry's answer.


If you're stuck with an older version, there are 2 ways to do this, in the easy case:

  • You can use mutable on the variable that are not part of the key
  • You can split your class in a Key Value pair (and use a std::map)

Now, the question is for the tricky case: what happens when the update actually modifies the key part of the object ? Your approach works, though I admit it's tedious.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • 4
    adding mutable on a non key member might be ok if its some internal/private class, but its still a dirty hack! As soon as the class is exposed to some users, id never dare to use mutable for members that are not meant to be mutable! Thats evil! – Marti Nito Sep 17 '15 at 16:00
24

In C++17 you can do better with extract(), thanks to 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));

This will avoid an extra copy of your type and an extra allocation/deallocation, and will also work with move-only types.

You can also extract by key. If the key is absent, this will return an empty node:

auto node = Set.extract(key);
if (node) // alternatively, !node.empty()
{
    node.value() = 42;
    Set.insert(std::move(node));
}
Barry
  • 286,269
  • 29
  • 621
  • 977
9

Update: Although the following is true as of now, the behavior is considered a defect and will be changed in the upcoming version of the standard. How very sad.


There are several points that make your question rather confusing.

  1. Functions can return values, classes can't. std::set is a class, and therefore cannot return anything.
  2. If you can call s.erase(iter), then iter is not a const_iterator. erase requires a non-const iterator.
  3. All member functions of std::set that return an iterator return a non-const iterator as long as the set is non-const as well.

You are allowed to change the value of an element of a set as long as the update doesn't change the order of elements. The following code compiles and works just fine.

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

If your update changes the order of elements, then you have to erase and reinsert.

avakar
  • 32,009
  • 9
  • 68
  • 103
  • That code does not compile, at least in VC10 - for the reasons I outlined in my post. This may have changed recently (been a "bug fix" to the standard), I thought it was in C++03, but I could be wrong. – Terry Mahaffey Feb 07 '10 at 19:38
  • 1
    Well, I'm looking at 23.1.2[lib.associative.reqmts] of C++03, table 69 and it says: "a.find(k): iterator; const_iterator for constant a". – avakar Feb 07 '10 at 19:42
  • And I just checked the draft for C++0x and there is no change (which is unsurprising and it would break a lot of code). – avakar Feb 07 '10 at 19:44
  • 1
    I don't have the C++03 pdf handy (it costs money). I thought this was fixed in C++03, but it could have been a post 03 fix. In n3000.pdf it's 23.2.4/6 which explains that for associative containers, iterator is a const iterator (and can be the same type as const_iterator). What compiler are you using? This behavior was implemented in VC9 as well (which was tracking C++03), which is why I thought the behavior was a C++03 bug fix. – Terry Mahaffey Feb 07 '10 at 19:46
  • 1
    Don't look at the table, look at the paragraph explain the requirements of "iterator" on an associative container. An iterator can be "const" without actually being named "const_iterator". – Terry Mahaffey Feb 07 '10 at 19:48
  • @Terry, right, although that's a change from C++03, where 23.1.2/6 (the equivalent of 23.3.4/6 in C++0x) only says "iterator of an associative container is of the bidirectional iterator category." I've built the snippet with msvc9 without problems, so it does indeed track C++03. – avakar Feb 07 '10 at 19:49
  • 4
    It is in the Standard library defect reports: http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#103. It doesn't compile with GCC, which makes a mention of DR 103 and typedefs both iterator and const_iterator to be the same type. - One proposed workaround for OP's problems, BTW, are const_cast and mutable members. – UncleBens Feb 07 '10 at 20:05
  • @Avakar - did you notice that for maps, C++03 specifies the value type is `pair`. Based on that, being allowed to change the set key seems like an oversight that the C++0x committee fixed. – R Samuel Klatchko Feb 07 '10 at 20:07
  • @Avakar - what happens in MSVC 9 when you do `*iter = 0;`? Does the operation throw? Does it allows the operation but cause problems in further uses of the container? – R Samuel Klatchko Feb 07 '10 at 20:09
  • 1
    @UncleBens, yes, I've found the DR too. I'm a little surprised it didn't make it into C++03 but was incorporated into the current draft. It is a rather severe change from my perspective as it will break otherwise correct code. – avakar Feb 07 '10 at 20:10
  • @R Samuel Klatchko, it will compile and run fine, but will most likely break somewhere down the line. – avakar Feb 07 '10 at 20:13
8

You may want to use an std::map instead. Use the portion of Element that affects the ordering the key, and put all of Element as the value. There will be some minor data duplication, but you will have easier (and possibly faster) updates.

Dev Null
  • 4,731
  • 1
  • 30
  • 46
3

I encountered the very same issue in C++11, where indeed ::std::set<T>::iterator is constant and thus does not allow to change its contents, even if we know the transformation will not affect the < invariant. You can get around this by wrapping ::std::set into a mutable_set type or write a wrapper for the content:

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

I find this much simpler and it works in 90% the cases without the user even noticing there to be something between the set and the actual type.

bitmask
  • 32,434
  • 14
  • 99
  • 159
  • Interesting idea, I'm going to try to remember this one. – Mark Ransom Aug 28 '14 at 22:25
  • 2
    @MarkRansom: Yes, but just to be extra sure, it should be noted, that this may only be used if the modification of the data stored behind the iterator is **guaranteed** not to change the ordering of the set. Otherwise, this is UB and it *will* break! (Just reiterating this to be sure nobody shoots themselves in the foot. I'm not implying you, in particular, don't realise this.) – bitmask Aug 29 '14 at 00:44
  • +1 this is ideal if you want unique sorted items based on some key (for which set is the appropriate container) but also want to keep some 'metadata' with them which can change – stijn Apr 27 '15 at 10:24
  • This approach doesn't work on my object. Specially on Pointer-like operator "operator->()" when I try to call my class method. – Erman Jul 29 '17 at 09:55
0

This is faster in some cases:

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

If the value is usually not already in the std::set then this can have better performance.

Dev Null
  • 4,731
  • 1
  • 30
  • 46
jcoffland
  • 5,238
  • 38
  • 43