Обновление С++ std :: set утомительно: я не могу изменить элемент на месте

Я считаю, что операция обновления на 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, создав свой собственный (который я не знаю точно, как это работает..)

Ответ 1

set возвращает const_iterators (стандарт говорит, что set<T>::iterator - const, и что set<T>::const_iterator и set<T>::iterator могут быть одного и того же типа - см. 23.2.4/6 в n3000.pdf) потому что это упорядоченный контейнер. Если он вернул обычный iterator, вам было бы позволено изменить значение элементов из контейнера, что может изменить порядок.

Ваше решение - это идиоматический способ изменения элементов в set.

Ответ 2

Есть два способа сделать это, в удобном случае:

  • Вы можете использовать mutable для переменной, которая не является частью ключа
  • Вы можете разделить свой класс на пару Key Value (и использовать std::map)

Теперь вопрос заключается в сложном случае: что происходит, когда обновление фактически изменяет часть объекта Key объекта? Ваш подход работает, хотя я признаю, что это утомительно.

Ответ 3

Обновление: Хотя на данный момент верно следующее: поведение считается defect и будет будут изменены в следующей версии стандарта. Как очень грустно.


Есть несколько моментов, которые делают ваш вопрос довольно запутанным.

  • Функции могут возвращать значения, классы не могут. std::set является классом и, следовательно, ничего не может вернуть.
  • Если вы можете позвонить s.erase(iter), то iter не является const_iterator. erase требуется инертный нетератор.
  • Все функции-члены 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;
}

Если ваше обновление изменяет порядок элементов, вам необходимо удалить и повторно вставить.

Ответ 4

Вы можете вместо этого использовать std::map. Используйте часть Element которая влияет на порядок ключей, и поместите весь Element в качестве значения. Будет небольшое дублирование данных, но у вас будут более простые (и, возможно, более быстрые) обновления.

Ответ 5

В С++ 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));
}

Ответ 6

Я столкнулся с той же проблемой в С++ 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% случаев без того, чтобы пользователь даже замечал, что есть что-то между набором и фактическим типом.

Ответ 7

Если ваш набор содержит объекты, которые вы хотите изменить, убедитесь, что вы сохранили их указатели в наборе.

Ну, это не мешает вам вставлять несколько объектов с одинаковым значением (ну, вы не можете вставлять один и тот же объект несколько раз), но это полезно, если вы хотите иметь контейнер с быстрой вставкой и удалением.

Ответ 8

Это быстрее в некоторых случаях:

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

Если значение обычно отсутствует в std::set это может повысить производительность.