Ustaw operację w c ++ (zaktualizuj istniejącą wartość)

21

Oto mój kod:

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

Nie mogę zaktualizować wartości ustawionej przez iterator. Chcę odjąć wartość całkowitą „sub” od wszystkich elementów zestawu.

Czy ktoś może mi pomóc, gdzie jest rzeczywisty problem i jakie byłoby faktyczne rozwiązanie?

Oto komunikat o błędzie:

error: assignment of read-only location it.std::_Rb_tree_const_iterator<int>::operator*()’
   28 |             *it=*it-sub;
      |             ~~~^~~~~~~~
Imtiaz Mehedi
źródło
1
Zaktualizuj do minimalnego odtwarzalnego przykładu .
Yunnosch
9
Elementy w zestawie można tylko czytać. Zmieniając jeden, możesz zmienić kolejność innych elementów w zestawie.
rafix07
3
Rozwiązaniem jest usunięcie iteratora i wstawienie nowego z kluczem *it - sub. Pamiętaj, że std::set::erase()zwraca nowy iterator, który musi być użyty w twoim przypadku, aby whilepętla działała poprawnie.
Scheff
2
@Scheff Czy możesz to zrobić, gdy jesteś nad serią? Czy nie może skończyć w nieskończonej pętli? Zwłaszcza podczas robienia czegoś istotnego dla aktualnie iterowanego zestawu, który umieszcza odwiedzane elementy tam, gdzie będą ponownie odwiedzane?
Yunnosch
1
Imtiaz Z ciekawości, na wypadek, gdyby następowało to zadanie, czy mógłbyś to zgłosić tutaj w komentarzu (zakładam, że jest to zadanie bez znaczenia dla niego złego, twoje pytanie jest w porządku)? Jak widać w moich komentarzach do odpowiedzi Scheffa, spekuluję na temat większego planu nauczycieli z tym. Z ciekawości.
Yunnosch

Odpowiedzi:

22

Kluczowe wartości elementów w a std::setconstnie bez powodu. Modyfikowanie ich może zniszczyć porządek, który jest niezbędny dla std::set.

Dlatego rozwiązaniem jest usunięcie iteratora i wstawienie nowego z kluczem *it - sub. Pamiętaj, że std::set::erase()zwraca nowy iterator, który musi być użyty w twoim przypadku, aby pętla while działała poprawnie.

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

Wynik:

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

Demo na żywo na coliru


Zmiany w std::settrakcie iteracji nie są ogólnie problemem, ale mogą powodować subtelne problemy.

Najważniejsze jest to, że wszystkie używane iteratory muszą pozostać nienaruszone lub nie można ich już używać. (Dlatego właśnie bieżącemu iteratorowi elementu kasującego przypisuje się wartość zwracaną, std::set::erase()która jest albo nienaruszonym iteratorem, albo końcem zestawu).

Oczywiście elementy można wstawić również za bieżącym iteratorem. Chociaż nie stanowi to problemu std::set, może przerwać pętlę mojego powyższego przykładu.

Aby to zademonstrować, zmieniłem nieco powyższą próbkę. Pamiętaj, że dodałem dodatkowy licznik, aby umożliwić zakończenie pętli:

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

Wynik:

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

Demo na żywo na coliru

Scheff
źródło
1
Czy to możliwe, że to działa tylko na odejmowanie czegoś, ale może skończyć się w nieskończonej pętli, jeśli operacja spowoduje ponowne wstawienie w miejscu, w którym będzie później odwiedzana ...?
Yunnosch
@Yunnosch Oprócz niebezpieczeństwa dla nieskończonej pętli, nie ma problemu z wstawieniem iteratorów za bieżącym iteratorem. Iteratory są stabilne w std::set. Konieczne może być rozważenie przypadku granicznego, że nowy iterator jest wstawiany bezpośrednio za skasowanym. - Zostanie pominięty po wstawieniu do pętli.
Scheff,
3
W C ++ 17 możesz extractwęzły, modyfikować ich klucze i przywracać je z powrotem do zestawu. Byłoby to bardziej wydajne, ponieważ pozwala uniknąć niepotrzebnych przydziałów.
Daniel Langr
Czy mógłbyś rozwinąć „Zostanie on pominięty po wstawieniu do pętli”. Myślę, że nie rozumiem tego.
Yunnosch
2
Innym problemem jest to, że wartość odjętego elementu może być taka sama, jak jedna z nieprzetworzonych wartości w std::set. Ponieważ nie możesz mieć tego samego elementu dwa razy, wstawienie pozostawi std::setniezmienione, a element stracisz później. Rozważmy na przykład zestaw danych wejściowych: za {10, 20, 30}pomocą add = 10.
ComicSansMS
6

Po prostu zastąp go innym zestawem

std::set<int> copy;

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

s.swap(copy);
acraig5075
źródło
5

Nie można mutować elementów std::setwedług projektu. Widzieć

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

Ponieważ zarówno iterator, jak i const_iterator są stałymi iteratorami (i w rzeczywistości mogą być tego samego typu), nie jest możliwe mutowanie elementów kontenera za pomocą iteratora zwróconego przez którąkolwiek z tych funkcji składowych.

To dlatego, że zestaw jest posortowany . Jeśli mutujesz element w posortowanej kolekcji, kolekcja musi zostać ponownie posortowana, co oczywiście jest możliwe, ale nie w języku C ++.

Twoje opcje to:

  1. Użyj innego rodzaju kolekcji (nieposortowane).
  2. Utwórz nowy zestaw i wypełnij go zmodyfikowanymi elementami.
  3. Usuń element std::set, zmodyfikuj go, a następnie wstaw ponownie. (Nie jest dobrym pomysłem, jeśli chcesz zmodyfikować każdy element)
x00
źródło
4

A std::setjest zwykle implementowane jako samo-równoważące się drzewo binarne w STL. *itjest wartością elementu używanego do uporządkowania drzewa. Gdyby można było go zmodyfikować, zamówienie stałoby się nieważne, dlatego nie można tego zrobić.

Jeśli chcesz zaktualizować element, musisz znaleźć ten element w zestawie, usunąć go i wstawić zaktualizowaną wartość elementu. Ale ponieważ musisz zaktualizować wartości wszystkich elementów, musisz usunąć i wstawić wszystkie elementy jeden po drugim.

Można to zrobić w jednym z dostępnych pętli sub > 0. S.erase(pos)usuwa iterator w pozycji posi zwraca następującą pozycję. Jeśli sub > 0zaktualizowana wartość, którą wstawisz, pojawi się przed wartością w nowym iteratorze w drzewie, ale jeśli sub <= 0, to zaktualizowana wartość pojawi się po wartości w nowym iteratorze w drzewie, a zatem skończysz na nieskończona pętla.

for (auto itr = S.begin(); itr != S.end(); )
{
    int val = *itr;
    itr = S.erase(itr);
    S.insert(val - sub);
}
lucieon
źródło
To dobry sposób ... Myślę, że to jedyny sposób, aby to zrobić. Wystarczy usunąć i ponownie wstawić.
Imtiaz Mehedi
3

Błąd właściwie wyjaśnia problem

Członkami std::setkontenera są const. Ich zmiana powoduje, że ich zamówienie jest nieważne.

Aby zmienić elementy std::set, będziesz musiał usunąć element i włożyć go ponownie po zmianie.

Alternatywnie możesz użyć, std::mapaby pokonać ten scenariusz.

P0W
źródło