Установить операцию в c ++ (обновить существующее значение)

21

Вот мой код:

 while (it!=s.end())  //here 's' is a set of stl and 'it' is iterator of set
    {   
        *it=*it-sub;    //'sub' is an int value
        it++;
    }

Я не могу обновить значение, установленное итератором. Я хочу вычесть целочисленное значение 'sub' из всех элементов множества.

Может ли кто-нибудь помочь мне, где актуальная проблема и каково будет реальное решение?

Вот сообщение об ошибке:

error: assignment of read-only location it.std::_Rb_tree_const_iterator<int>::operator*()’
   28 |             *it=*it-sub;
      |             ~~~^~~~~~~~
Имтиаз Мехеди
источник
1
Пожалуйста, обновитесь до минимального воспроизводимого примера .
Yunnosch
9
Элементы в наборе можно только читать. Изменяя один, вы переупорядочиваете другие элементы в наборе.
rafix07
3
Решение состоит в том, чтобы стереть итератор и вставить новый с ключом *it - sub. Пожалуйста, обратите внимание, что std::set::erase()возвращает новый итератор, который должен использоваться в вашем случае для поддержания whileправильной работы цикла.
Шефф
2
@Scheff Можете ли вы сделать это, пока вы итерируете над сетом? Разве это не может закончиться бесконечной петлей? Особенно, когда вы делаете что-то релевантное текущему итерированному набору, который помещает посещенные элементы туда, где они будут снова посещаться?
Yunnosch
1
Imtiaz Из любопытства, в случае, если есть продолжение этого задания, не могли бы вы сообщить об этом здесь в комментарии (я предполагаю, что это назначение, не имея в виду ничего плохого, ваш вопрос в порядке)? Как вы можете видеть в моих комментариях к ответу Шеффа, я размышляю о более широком плане учителей с этим. Просто любопытство.
Yunnosch

Ответы:

22

Ключевые значения элементов в a std::setявляются constвеской причиной. Изменение их может разрушить порядок, который необходим для std::set.

Следовательно, решение состоит в том, чтобы стереть итератор и вставить новый с ключом *it - sub. Обратите внимание, что std::set::erase()возвращается новый итератор, который должен использоваться в вашем случае, чтобы цикл while работал правильно.

#include<iostream>
#include<set>

template <typename T>
std::ostream& operator<<(std::ostream &out, const std::set<T> &values)
{
  const char *sep = "{ ";
  for (const T &value : values) { out << sep << value; sep = ", "; }
  return out << " }";
}

int main()
{
  std::set<int> test{ 11, 12, 13, 14, 15 };
  std::cout << "test: " << test << '\n';
  const int sub = 10;
  std::set<int>::iterator iter = test.begin();
  while (iter != test.end()) {
    const int value = *iter;
    iter = test.erase(iter);
    test.insert(value - sub);
  }
  std::cout << "test: " << test << '\n';
}

Вывод:

test: { 11, 12, 13, 14, 15 }
test: { 1, 2, 3, 4, 5 }

Живая Демо на Колиру


Изменения во std::setвремя итерации по нему не являются проблемой в целом, но могут вызвать тонкие проблемы.

Наиболее важным фактом является то, что все используемые итераторы должны быть сохранены или могут больше не использоваться. (Вот почему текущему итератору элемента стирания присваивается возвращаемое значение, std::set::erase()которое является либо неповрежденным итератором, либо концом набора.)

Конечно, элементы могут быть вставлены также за текущим итератором. Хотя это не проблема, std::setэто может нарушить цикл моего примера выше.

Чтобы продемонстрировать это, я немного изменил приведенный выше пример. Пожалуйста, обратите внимание, что я добавил дополнительный счетчик для предоставления завершения цикла:

#include<iostream>
#include<set>

template <typename T>
std::ostream& operator<<(std::ostream &out, const std::set<T> &values)
{
  const char *sep = "{ ";
  for (const T &value : values) { out << sep << value; sep = ", "; }
  return out << " }";
}

int main()
{
  std::set<int> test{ 11, 12, 13, 14, 15 };
  std::cout << "test: " << test << '\n';
  const int add = 10;
  std::set<int>::iterator iter = test.begin();
  int n = 7;
  while (iter != test.end()) {
    if (n-- > 0) {
      const int value = *iter;
      iter = test.erase(iter);
      test.insert(value + add);
    } else ++iter;
  }
  std::cout << "test: " << test << '\n';
}

Вывод:

test: { 11, 12, 13, 14, 15 }
test: { 23, 24, 25, 31, 32 }

Живая Демо на Колиру

Шефф
источник
1
Возможно ли, что это работает только для вычитания чего-либо, но может закончиться бесконечным циклом, если операция вызывает повторную вставку в месте, где она будет посещена позже ....?
Yunnosch
@Yunnosch Помимо опасности для бесконечного цикла, нет проблем вставить итераторы за текущим итератором. Итераторы стабильны в std::set. Возможно, потребуется рассмотреть граничный случай, когда новый итератор вставляется непосредственно за стертым. - Он будет пропущен после вставки в цикл.
Шефф
3
В C ++ 17 вы можете extractузлы, изменять их ключи и возвращать их обратно в set. Это было бы более эффективно, поскольку позволяет избежать ненужных распределений.
Даниэль Лангр
Не могли бы вы уточнить «Это будет пропущено после вставки в цикл». Я думаю, что я не понимаю вашу точку зрения там.
Yunnosch
2
Другая проблема заключается в том, что значение вычитаемого элемента может совпадать с одним из еще не обработанных значений в std::set. Поскольку вы не можете иметь один и тот же элемент дважды, вставка просто оставит std::setнеизменным, и вы потеряете элемент позже. Рассмотрим, например, входной набор: {10, 20, 30}с add = 10.
ComicSansMS
6

Просто заменить его другим набором

std::set<int> copy;

for (auto i : s)
    copy.insert(i - sub);

s.swap(copy);
acraig5075
источник
5

Вы не можете мутировать элементы std::setдизайна. Видеть

https://en.cppreference.com/w/cpp/container/set/begin

Поскольку итератор и const_iterator являются постоянными итераторами (и на самом деле могут иметь один и тот же тип), невозможно изменить элементы контейнера через итератор, возвращаемый любой из этих функций-членов.

Это потому, что набор отсортирован . Если вы изменяете элемент в отсортированной коллекции, коллекция должна быть отсортирована снова, что, конечно, возможно, но не в C ++.

Ваши варианты:

  1. Используйте другой тип коллекции (не отсортированный).
  2. Создайте новый набор и заполните его измененными элементами.
  3. Удалите элемент std::set, измените его, затем вставьте снова. (Это не очень хорошая идея, если вы хотите изменить каждый элемент)
x00
источник
4

A std::setобычно реализуется как самобалансирующееся двоичное дерево в STL. *itзначение элемента, используемого для упорядочения дерева. Если бы было возможно изменить его, заказ стал бы недействительным, следовательно, это невозможно сделать.

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

Это можно сделать за один цикл sub > 0. S.erase(pos)удаляет итератор в позиции posи возвращает следующую позицию. Если sub > 0обновленное значение, которое вы вставите, будет предшествовать значению в новом итераторе в дереве, но если sub <= 0, тогда обновленное значение будет следовать после значения в новом итераторе в дереве, и, следовательно, вы получите бесконечный цикл.

for (auto itr = S.begin(); itr != S.end(); )
{
    int val = *itr;
    itr = S.erase(itr);
    S.insert(val - sub);
}
lucieon
источник
Это хороший способ ... Я думаю, что это единственный способ сделать это. Просто удалите и снова вставьте.
Имтиаз Мехеди
3

Ошибка в значительной степени объясняет проблему

Члены std::setконтейнера есть const. Изменение их делает их соответствующий порядок недействительным.

Для изменения элементов std::setвам нужно будет стереть элемент и повторно вставить его после его изменения.

В качестве альтернативы, вы можете использовать, std::mapчтобы преодолеть этот сценарий.

P0W
источник