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 int
no 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 x
parte 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 const
referê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 const
referê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, elem
armazena 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 const
referê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::vector
modelo é especializada para bool
, com uma implementação que pacotes os bool
s 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:
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 int
s, double
s, etc.), é possível usar um formulário um pouco simplificado:
for (auto elem : container) // capture by value
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 T
ser 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:
Para observar os elementos, use:
for (const auto& elem : container)
Para modificar os elementos no lugar, use:
for (auto&& elem : container)