Seguindo com as informações iniciadas em
post anterior
sobre os novos recursos do C++11, veremos agora algumas das mais simples e
práticas destas novidades, mesmo para programadores iniciantes.
Inferência de tipo com auto
A palavra reservada
auto sempre existiu na linguagem C++ pois foi herdada das suas antecessoras. Ainda assim era uma das menos utilizadas, pois indicava que uma variável local deveria usar alocação automática. Como este é o armazenamento padrão para este caso, colocar ou omitir este modificador costumava fazer pouca diferença.
Já no C++11 foi definido outro significado bem mais útil para o
auto. Agora ele é usado para declarar variáveis cujo tipo vai ser descoberto automaticamente pelo compilador a partir do valor que está sendo usado na inicialização desta variável (a inicialização é obrigatória).
auto v1=sin(1.5); // 'v1' é um double
auto c='X'; // 'c' é um char
map<int, string> m;
auto i=m.begin(); // 'i' é um map<int, string>::iterator
Esta facilidade é mais útil quando inicializamos uma variável com o valor retornado por uma função, pois delegamos ao compilador a tarefa de descobrir qual o tipo retornado. Principalmente no caso de iterators, que costumam ter uma declaração bem extensa.
Sintaxe uniforme de inicialização
A inicialização de variáveis em C++ pode ser feita usando-se sintaxes diferenciadas. Em alguns casos, o programador tem escolha, em outros não. Vejamos alguns exemplos:
int n=100;
int vet[]={10, 20, 30, 40};
float var(1.5);
string s("aeiou");
Nos dois primeiros casos, herdou-se da linguagem C uma sintaxe para inicialização que faz uso do mesmo símbolo do operador de atribuição (=). O primeiro exemplo é a forma clássica de inicialização para variáveis simples. O segundo caso, com os valores entre chaves, é empregada para inicializar arrays e variáveis de estrutura.
Os dois últimos casos são uma sintaxe específica do C++, onde o valor é fornecido entre parênteses. No terceiro caso está sendo simulada a chamada de um construtor, pois não existe um construtor a ser executado visto que
float é um tipo fundamental. Já no último exemplo, a string está sendo inicializada pelo construtor da classe, com o valor passado como argumento para este construtor.
Com o objetivo de unificar as formas de inicialização, foi adicionado ao C++ uma nova sintaxe. Coloca-se o valor entre chaves, sem o símbolo de atribuição:
int n{100};
int vet[]{10, 20, 30, 40};
float var{1.5};
string s{"aeiou"};
Falando-se historicamente não me parece uma sintaxe tão nova assim, pois já era usada na linguagem B, precursora do C e portanto "
avó" do C++, como pode ser visto
aqui.
Com este novo formato conseguimos alguns recursos adicionais úteis, de forma que a diferença não ficou apenas na sintaxe. Ele pode ser utilizado para prevenir o
narrowing, que é a atribuição de um valor de tipo mais "amplo" (em capacidade de armazenamento) para um mais "estreito", que possa levar a perda de valores. Por exemplo, usar um
float para inicializar um
int:
float var;
cin >> var;
int a = var; // Ok
int b{var}; // Warning: narrowing conversion from 'float' to 'int'
Os formatos antigos aceitavam este estreitamento, mas na nova sintaxe vai gerar uma advertência. Sou da opinião que este tipo de alerta serve para construir software mais seguro. Uma das (muitas) coisas que gosto na linguagem C# é que ela impede
narrowing implícito em qualquer situação (inicialização, atribuição, operações aritméticas, passagem de parâmetros, ...). Se o programador tem certeza que não vai haver perda de informação, precisa fazer um
cast explícito. Como no C++ isto só vale para a inicialização, o ganho é limitado.
Outro recurso é que pode-se inicializar facilmente uma variável ou mesmo todos os elementos de um array com valor
default do tipo, o que para os tipos numéricos fundamentais é 0. Basta deixar as chaves vazias, como no exemplo:
long long int bign{}; // bign=0LL;
float vet[1000]{}; // Todos os 1000 elementos com valor 0.0
Casos em que não recomendo usar:
- Para inicializar a variável de controle de um laço for: Em vez de for(int i{0}; ...) eu continuo escrevendo for(int i=0; ..;). Não é só pelo costume adquirido, mas por que em alguns casos eu tenho a variável de controle do loop já definida anteriormente (principalmente iterators), aí usaria apenas a atribuição. Mas aqui já é uma questão de estilo de escrita. E com o range based for disponível no C++11, esta situação é cada vez menos comum.
- Substituindo indiscrinadamente os parênteses na chamada de construtores, exceto construtores sem parâmetros (quando parênteses vazios não funcionam). Particularmente deve-se ter cuidado com classes que tenham construtor com initializer_list como parâmetro, pois este é o que será usado, como no exemplo comentado abaixo:
vector<int> vet(10); // Cria um vector com 10 elementos
vector<int> vet{10}; // Cria um vector com 1 elemento de valor 10
Não junte as duas coisas, exceto se estiver convicto!
Ansioso por usar as novidades, lá fui eu querendo escrever
auto e
T t{val} em todos os meus códigos. Até que compilei um arquivo com algo simples assim:
auto flag{true};
while(flag){
// ...
}
Para mim, estava definindo uma variável
bool inicializada com valor
true. Mas não... Deu erro de compilação na condição do while.
Motivo: No contexto em que uma expressão entre {} puder ser usada como uma
initializer_list será isto que o compilador vai usar. Então estava definindo uma
initializer_list<bool> com um único valor igual
a true. Precisei corrigir alterando para
bool flag{true};
Fica a dica! Tomando-se alguns cuidados, podemos usar os novos recursos sem medo.