Qual é a maneira correta de usar o intervalo do C ++ 11?

211

Qual é a maneira correta de usar o intervalo do C ++ 11 for?

Qual sintaxe deve ser usada? for (auto elem : container), ou for (auto& elem : container)ou for (const auto& elem : container)? Ou algum outro?

Mr.C64
fonte
6
A mesma consideração se aplica aos argumentos da função.
Maxim Egorushkin
3
Na verdade, isso tem pouco a ver com o intervalo para. O mesmo pode ser dito de qualquer um auto (const)(&) x = <expr>;.
Matthieu M.
2
@ MatthieuM: Isso tem muito a ver com o intervalo, é claro! Considere um iniciante que vê várias sintaxes e não pode escolher qual formulário usar. O objetivo das "perguntas e respostas" era tentar esclarecer e explicar as diferenças de alguns casos (e discutir casos que compilam muito bem, mas são meio ineficientes devido a cópias profundas inúteis etc.).
Mr.C64
2
@ Mr.C64: No que me diz respeito, isso tem mais a ver auto, em geral, do que com base no intervalo; você pode usar perfeitamente o intervalo com base, sem nenhum auto! for (int i: v) {}está perfeitamente bem. Obviamente, a maioria dos pontos que você levanta em sua resposta pode ter mais a ver com o tipo do que com auto... mas, a partir da pergunta, não está claro onde está o ponto de dor. Pessoalmente, competiria por me afastar autoda questão; ou talvez explique que, se você usa autoou nomeia explicitamente o tipo, a questão é focada em valor / referência.
precisa
1
@ MatthieuM .: Estou aberto a alterar o título ou editar a pergunta de alguma forma que possa torná-la mais clara ... Novamente, meu foco foi discutir várias opções de sintaxe baseada em intervalo (mostrando o código que compila, mas é ineficiente, código que falha na compilação etc.) e tentando oferecer alguma orientação a alguém (especialmente no nível iniciante) que se aproxima do C ++ 11 baseado em intervalo para loops.
Mr.C64

Respostas:

389

Vamos começar a diferenciar entre observar os elementos no contêiner e modificá- los no lugar.

Observando os elementos

Vamos considerar um exemplo simples:

vector<int> v = {1, 3, 5, 7, 9};

for (auto x : v)
    cout << x << ' ';

O código acima imprime os elementos intno vector:

1 3 5 7 9

Agora considere outro caso, no qual os elementos vetoriais não são apenas números inteiros simples, mas instâncias de uma classe mais complexa, com construtor de cópia personalizado, etc.

// A sample test class, with custom copy semantics.
class X
{
public:
    X() 
        : m_data(0) 
    {}

    X(int data)
        : m_data(data)
    {}

    ~X() 
    {}

    X(const X& other) 
        : m_data(other.m_data)
    { cout << "X copy ctor.\n"; }

    X& operator=(const X& other)
    {
        m_data = other.m_data;       
        cout << "X copy assign.\n";
        return *this;
    }

    int Get() const
    {
        return m_data;
    }

private:
    int m_data;
};

ostream& operator<<(ostream& os, const X& x)
{
    os << x.Get();
    return os;
}

Se usarmos a for (auto x : v) {...}sintaxe acima com esta nova classe:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (auto x : v)
{
    cout << x << ' ';
}

a saída é algo como:

[... copy constructor calls for vector<X> initialization ...]

Elements:
X copy ctor.
1 X copy ctor.
3 X copy ctor.
5 X copy ctor.
7 X copy ctor.
9

Como pode ser lido a partir da saída, as chamadas do construtor de cópia são feitas durante as iterações de loop baseadas em intervalo.
Isso ocorre porque estamos capturando os elementos do contêiner por valor (a auto xparte emfor (auto x : v) ).

Este é um código ineficiente , por exemplo, se esses elementos são instâncias std::string, alocações de memória heap podem ser feitas, com viagens caras ao gerenciador de memória, etc. Isso é inútil se queremos apenas observar os elementos em um contêiner.

Portanto, uma sintaxe melhor está disponível: captura por constreferência , ou seja const auto&:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (const auto& x : v)
{ 
    cout << x << ' ';
}

Agora a saída é:

 [... copy constructor calls for vector<X> initialization ...]

Elements:
1 3 5 7 9

Sem nenhuma chamada de construtor de cópia espúria (e potencialmente cara).

Assim, quando observando elementos em um recipiente (ou seja, para acesso somente leitura), a seguinte sintaxe é bom para simples barato-a-cópia tipos, como int, double, etc .:

for (auto elem : container) 

Caso contrário , a captura por constreferência é melhor no caso geral , para evitar chamadas de construtor de cópia inúteis (e potencialmente caras):

for (const auto& elem : container) 

Modificando os elementos no contêiner

Se quisermos modificar os elementos em um contêiner usando o intervalo for, os itens acima for (auto elem : container)efor (const auto& elem : container) sintaxes as incorretas.

De fato, no primeiro caso, elemarmazena uma cópia do elemento original, para que as modificações feitas sejam perdidas e não armazenadas persistentemente no contêiner, por exemplo:

vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)  // <-- capture by value (copy)
    x *= 10;      // <-- a local temporary copy ("x") is modified,
                  //     *not* the original vector element.

for (auto x : v)
    cout << x << ' ';

A saída é apenas a sequência inicial:

1 3 5 7 9

Em vez disso, uma tentativa de usar for (const auto& x : v) apenas falha na compilação.

O g ++ gera uma mensagem de erro mais ou menos assim:

TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
          x *= 10;
            ^

A abordagem correta neste caso é capturar por não constreferência:

vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
    x *= 10;

for (auto x : v)
    cout << x << ' ';

A saída é (como esperado):

10 30 50 70 90

Essa for (auto& elem : container)sintaxe também funciona para tipos mais complexos, por exemplo, considerando um vector<string>:

vector<string> v = {"Bob", "Jeff", "Connie"};

// Modify elements in place: use "auto &"
for (auto& x : v)
    x = "Hi " + x + "!";

// Output elements (*observing* --> use "const auto&")
for (const auto& x : v)
    cout << x << ' ';

a saída é:

Hi Bob! Hi Jeff! Hi Connie!

O caso especial de iteradores de proxy

Suponha que temos vector<bool>ae queremos inverter o estado booleano lógico de seus elementos, usando a sintaxe acima:

vector<bool> v = {true, false, false, true};
for (auto& x : v)
    x = !x;

O código acima falha ao compilar.

O g ++ gera uma mensagem de erro semelhante a esta:

TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
 type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'
     for (auto& x : v)
                    ^

O problema é que o std::vectormodelo é especializada para bool, com uma implementação que pacotes os bools ao espaço optimize (cada valor booleano é armazenado em um pouco, oito bits de "booleanas" em um byte).

Por esse motivo (como não é possível retornar uma referência a um único bit), vector<bool>utiliza o chamado padrão "proxy iterator" . Um "iteração proxy" é uma iteração que, quando desreferenciado, se não se obter um comum bool &, mas em vez disso os retornos (em valor) um objeto temporária , que é uma classe de proxy conversível abool . (Veja também esta pergunta e respostas relacionadas aqui no StackOverflow.)

Para modificar no lugar os elementos de vector<bool>, um novo tipo de sintaxe (usando auto&&) deve ser usado:

for (auto&& x : v)
    x = !x;

O código a seguir funciona bem:

vector<bool> v = {true, false, false, true};

// Invert boolean status
for (auto&& x : v)  // <-- note use of "auto&&" for proxy iterators
    x = !x;

// Print new element values
cout << boolalpha;        
for (const auto& x : v)
    cout << x << ' ';

e saídas:

false true true false

Observe que a for (auto&& elem : container)sintaxe também funciona nos outros casos de iteradores comuns (sem proxy) (por exemplo, para um vector<int>ou umvector<string> ).

(Como uma observação lateral, a sintaxe de "observação" acima mencionada for (const auto& elem : container)também funciona bem para o caso do iterador de proxy.)

Resumo

A discussão acima pode ser resumida nas seguintes diretrizes:

  1. Para observar os elementos, use a seguinte sintaxe:

    for (const auto& elem : container)    // capture by const reference
    • Se os objetos são baratos para copiar (como ints, doubles, etc.), é possível usar um formulário um pouco simplificado:

      for (auto elem : container)    // capture by value
  2. Para modificar os elementos no lugar, use:

    for (auto& elem : container)    // capture by (non-const) reference
    • Se o contêiner usar "iteradores de proxy" (como std::vector<bool>), use:

      for (auto&& elem : container)    // capture by &&

Obviamente, se houver uma necessidade de fazer uma cópia local do elemento dentro do corpo do loop, capturar por value ( for (auto elem : container)) é uma boa opção.


Notas adicionais sobre código genérico

No código genérico , como não podemos fazer suposições sobre o tipo genérico Tser barato de copiar, no modo de observação é seguro usá-lo sempre for (const auto& elem : container).
(Isso não acionará cópias inúteis potencialmente caras, funcionará bem também para tipos baratos de copiar int, como também para contêineres usando iteradores de proxy, como std::vector<bool>.)

Além disso, no modo de modificação , se queremos que o código genérico funcione também no caso de iteradores de proxy, a melhor opção é for (auto&& elem : container).
(Isso funcionará bem também para contêineres que usam iteradores não proxy comuns, como std::vector<int>ou std::vector<string>.)

Portanto, no código genérico , as seguintes diretrizes podem ser fornecidas:

  1. Para observar os elementos, use:

    for (const auto& elem : container)
  2. Para modificar os elementos no lugar, use:

    for (auto&& elem : container)
Mr.C64
fonte
7
Nenhum conselho para contextos genéricos? :
R.
11
Por que nem sempre usar auto&&? Existe um const auto&&?
Martin Ba
1
Eu acho que você está perdendo o caso em que você realmente precisa de uma cópia dentro do loop?
precisa saber é o seguinte
6
"Se o contêiner usa" iteradores de proxy "" - e você sabe que usa "iteradores de proxy" (que pode não ser o caso no código genérico). Então, acho que o melhor é de fato auto&&, uma vez que cobre auto&igualmente bem.
Christian Rau
5
Obrigado, essa foi realmente uma ótima "introdução ao curso intensivo" à sintaxe de e algumas dicas para o intervalo, para um programador de C #. +1.
precisa saber é o seguinte
17

Não há maneira correta de usar for (auto elem : container), ou for (auto& elem : container)ou for (const auto& elem : container). Você acabou de expressar o que deseja.

Deixe-me elaborar sobre isso. Vamos dar um passeio.

for (auto elem : container) ...

Este é o açúcar sintático para:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Observe that this is a copy by value.
    auto elem = *it;

}

Você pode usar este se o seu contêiner contiver elementos que são baratos para copiar.

for (auto& elem : container) ...

Este é o açúcar sintático para:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Now you're directly modifying the elements
    // because elem is an lvalue reference
    auto& elem = *it;

}

Use isso quando desejar gravar diretamente nos elementos no contêiner, por exemplo.

for (const auto& elem : container) ...

Este é o açúcar sintático para:

for(auto it = container.begin(); it != container.end(); ++it) {

    // You just want to read stuff, no modification
    const auto& elem = *it;

}

Como o comentário diz, apenas para leitura. E é isso, tudo está "correto" quando usado corretamente.


fonte
2
Eu pretendia dar algumas orientações, com exemplos de códigos compilando (mas sendo ineficientes) ou falhando em compilar, explicando o motivo e tentando propor algumas soluções.
Mr.C64
2
@ Mr.C64 Oh, desculpe - acabei de perceber que essa é uma daquelas perguntas do tipo FAQ. Eu sou novo neste site. Desculpas! Sua resposta é ótima, eu a votei - mas também queria fornecer uma versão mais concisa para aqueles que querem o essencial . Felizmente, não estou me intrometendo.
1
@ Mr.C64, qual é o problema com o OP respondendo à pergunta também? É apenas mais uma resposta válida.
precisa saber é o seguinte
1
@mfontanini: Não há absolutamente nenhum problema se alguém postar alguma resposta, ainda melhor que a minha. O objetivo final é dar uma contribuição de qualidade à comunidade (especialmente para iniciantes que podem se sentir meio perdidos diante de diferentes sintaxes e opções diferentes que o C ++ oferece).
Mr.C64
4

O meio correto é sempre

for(auto&& elem : container)

Isso garantirá a preservação de toda a semântica.

Cachorro
fonte
6
Mas e se o contêiner retornar apenas referências modificáveis ​​e eu quiser deixar claro que não desejo modificá-las no loop? Eu não deveria então usar auto const &para esclarecer minha intenção?
RedX
@ RedX: O que é uma "referência modificável"?
Lightness Races in Orbit
2
@ RedX: As referências nunca são conste nunca são mutáveis. Enfim, minha resposta para você é sim, eu faria .
Lightness Races in Orbit
4
Embora isso possa funcionar, acho que este é um péssimo conselho em comparação com a abordagem mais sutil e considerada dada pela excelente e abrangente resposta do Sr. C64 dada acima. Reduzir para o denominador menos comum não é para isso que serve o C ++.
precisa saber é o seguinte
6
Esta proposta evolução da linguagem concorda com esta resposta "pobre": open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Luc Hermitte
1

Embora a motivação inicial do loop range-for possa ter sido fácil de iterar sobre os elementos de um contêiner, a sintaxe é genérica o suficiente para ser útil mesmo para objetos que não são puramente contêineres.

O requisito sintático para o loop for é esse range_expressionsuporte begin()e end()como funções - como funções de membro do tipo que ele avalia ou como funções de não-membro que levam uma instância do tipo.

Como um exemplo artificial, pode-se gerar um intervalo de números e iterar no intervalo usando a seguinte classe.

struct Range
{
   struct Iterator
   {
      Iterator(int v, int s) : val(v), step(s) {}

      int operator*() const
      {
         return val;
      }

      Iterator& operator++()
      {
         val += step;
         return *this;
      }

      bool operator!=(Iterator const& rhs) const
      {
         return (this->val < rhs.val);
      }

      int val;
      int step;
   };

   Range(int l, int h, int s=1) : low(l), high(h), step(s) {}

   Iterator begin() const
   {
      return Iterator(low, step);
   }

   Iterator end() const
   {
      return Iterator(high, 1);
   }

   int low, high, step;
}; 

Com a seguinte mainfunção,

#include <iostream>

int main()
{
   Range r1(1, 10);
   for ( auto item : r1 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r2(1, 20, 2);
   for ( auto item : r2 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r3(1, 20, 3);
   for ( auto item : r3 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;
}

seria possível obter a seguinte saída.

1 2 3 4 5 6 7 8 9 
1 3 5 7 9 11 13 15 17 19 
1 4 7 10 13 16 19 
R Sahu
fonte