Definir operação em c ++ (atualizar valor existente)

21

Aqui está o meu código:

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

Não consigo atualizar o valor de definido pelo iterador. Eu quero subtrair um valor inteiro 'sub' de todo o elemento do conjunto.

Alguém pode me ajudar onde está o problema real e qual seria a solução real?

Aqui está a mensagem de erro:

error: assignment of read-only location it.std::_Rb_tree_const_iterator<int>::operator*()’
   28 |             *it=*it-sub;
      |             ~~~^~~~~~~~
Imtiaz Mehedi
fonte
11
Atualize para um exemplo reproduzível mínimo .
Yunnosch 16/01
9
Os elementos do conjunto podem ser lidos apenas. Ao modificar um, você reordena outros itens no conjunto.
rafix07 16/01
3
A solução é apagar o iterador e inserir um novo com a chave *it - sub. Observe que std::set::erase()retorna um novo iterador que deve ser usado no seu caso para manter o whileloop funcionando corretamente.
Scheff
2
@Scheff Você pode fazer isso enquanto repassa um set? Não poderia terminar em um loop sem fim? Especialmente quando se faz algo relevante ao conjunto iterado de forma atual que coloca os elementos visitados onde eles serão visitados novamente?
Yunnosch 16/01
11
Imtiaz Por curiosidade, se houver um acompanhamento para esta tarefa, você poderia denunciá-la aqui em um comentário (presumo que seja uma tarefa sem significar nada de ruim por isso, sua pergunta está correta)? Como você pode ver nos meus comentários sobre a resposta de Scheff, estou especulando sobre o plano maior dos professores com isso. Só curiosidade.
Yunnosch 16/01

Respostas:

22

Os principais valores dos elementos em a std::setsão constpor um bom motivo. Modificá-los pode destruir a ordem que é essencial para a std::set.

Portanto, a solução é apagar o iterador e inserir um novo com a chave *it - sub. Observe que std::set::erase()retorna um novo iterador que deve ser usado no seu caso para manter o loop while funcionando corretamente.

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

Resultado:

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

Demonstração ao vivo no coliru


As alterações std::setdurante a iteração sobre ele não são um problema em geral, mas podem causar problemas sutis.

O fato mais importante é que todos os iteradores usados ​​devem ser mantidos intactos ou não podem mais ser usados. (É por isso que o iterador atual do elemento de apagamento é designado com o valor de retorno std::set::erase()que é um iterador intacto ou o final do conjunto.)

Obviamente, é possível inserir elementos também atrás do iterador atual. Embora isso não seja um problema, std::setele pode quebrar o loop do meu exemplo acima.

Para demonstrá-lo, alterei um pouco a amostra acima. Observe que eu adicionei um contador adicional para conceder o término do loop:

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

Resultado:

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

Demonstração ao vivo no coliru

Scheff
fonte
11
É possível que isso funcione apenas para subtrair algo, mas possa terminar em um loop infinito se a operação causar reinserção em um local onde será visitada posteriormente ....?
Yunnosch 16/01
@Yunnosch Além do perigo de loop infinito, não há problema em inserir iteradores atrás do iterador atual. Os iteradores são estáveis ​​no std::set. Pode ser necessário considerar o caso de borda em que o novo iterador é inserido diretamente atrás do apagado. - Será ignorado após a inserção no loop.
Scheff
3
No C ++ 17, você pode extractnós, modificar suas chaves e retorná-las ao conjunto. Seria mais eficiente, pois evita alocações desnecessárias.
Daniel Langr 16/01
Você poderia elaborar "Será ignorado após a inserção no loop". Eu acho que não entendi o seu ponto.
Yunnosch 16/01
2
Outra questão é que o valor de um elemento subtraído pode ser o mesmo que um dos valores ainda não processados ​​no std::set. Como você não pode ter o mesmo elemento duas vezes, a inserção deixará o std::setinalterado e você perderá o elemento mais tarde. Considere, por exemplo, o conjunto de entrada: {10, 20, 30}with add = 10.
ComicSansMS 16/01
6

Simples de substituí-lo por outro conjunto

std::set<int> copy;

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

s.swap(copy);
acraig5075
fonte
5

Você não pode alterar os elementos std::setpor design. Vejo

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

Como o iterador e o const_iterator são iteradores constantes (e podem de fato ser do mesmo tipo), não é possível alterar os elementos do contêiner por meio de um iterador retornado por qualquer uma dessas funções-membro.

Isso ocorre porque o conjunto está classificado . Se você alterar o elemento em uma coleção classificada, a coleção deverá ser classificada novamente, o que é obviamente possível, mas não a maneira C ++.

Suas opções são:

  1. Use outro tipo de coleção (não classificado).
  2. Crie um novo conjunto e preencha-o com elementos modificados.
  3. Remova um elemento std::set, modifique-o e insira novamente. (Não é uma boa ideia se você deseja modificar todos os elementos)
x00
fonte
4

A std::seté normalmente implementado como uma árvore binária de auto-equilíbrio no STL. *ité o valor do elemento que é usado para ordenar a árvore. Se fosse possível modificá-lo, o pedido se tornaria inválido, portanto, não é possível fazer isso.

Se você deseja atualizar um elemento, é necessário encontrar esse elemento no conjunto, removê-lo e inserir o valor atualizado do elemento. Mas como você precisa atualizar os valores de todos os elementos, é necessário apagar e inserir todos os elementos, um por um.

É possível fazer isso em um loop for fornecido sub > 0. S.erase(pos)remove o iterador na posição pose retorna a seguinte posição. Se sub > 0o valor atualizado que você inserir será anterior ao valor no novo iterador na árvore, mas se sub <= 0, então, o valor atualizado virá após o valor no novo iterador na árvore e, portanto, você terminará em um Loop infinito.

for (auto itr = S.begin(); itr != S.end(); )
{
    int val = *itr;
    itr = S.erase(itr);
    S.insert(val - sub);
}
lucieon
fonte
Essa é uma boa maneira ... Eu acho que é a única maneira de fazer isso. Apenas exclua e insira novamente.
Imtiaz Mehedi 17/01
3

O erro explica praticamente o problema

Os membros do std::setcontêiner são const. Mudá-los invalida a respectiva ordem.

Para alterar os elementos std::set, você precisará apagar o item e reinserir depois que ele for alterado.

Como alternativa, você pode usar std::mappara superar esse cenário.

P0W
fonte