16.07
2020
No post anterior falamos sobre std::for_each. Mas percebeu que não dá pra alterar o conteúdo do container dessa forma? A solução é o std::tranform. Vamos começar com um exemplo de como o for_each não funciona. Um programa para incrementar todos os elementos de um std::list:
#include <algorithm>
#include <iostream>
#include <list>
void incr(int i) { i++; }
void fcout(int i) { std::cout << i << " "; }
int main() {
std::list<int> l;
l.push_back(15);
l.push_back(13);
l.push_back(18);
std::cout << "before: ";
for_each(l.begin(), l.end(), fcout);
std::cout << std::endl;
for_each(l.begin(), l.end(), incr);
std::cout << "after: ";
for_each(l.begin(), l.end(), fcout);
std::cout << std::endl;
}
A saída:
before: 15 13 18
after: 15 13 18
Note que não houve alteração alguma no std::list. Isso porque o for_each não foi feito pra fazer alterações. Para isso existe o std::transform. Vamos para um código que faz o que queríamos.
#include<algorithm>
#include<iostream>
#include <list>
int incr(int i) { return ++i; }
void fcout(int i) { std::cout << i << " "; }
int main() {
std::list<int> l, r;
l.push_back(15);
l.push_back(17);
l.push_back(12);
r.resize(l.size());
std::cout << "before: ";
std::for_each(l.begin(), l.end(), fcout);
std::cout << std::endl;
std::transform(l.begin(), l.end(), r.begin(), incr);
std::cout << "after: ";
std::for_each(r.begin(), r.end(), fcout);
std::cout << std::endl;
}
E a saída:
before: 15 17 12
after: 16 18 13
Conforme esperado todos os elementos foram incrementados.
A síntaxe do std::transform é bem parecida com a do std::for_each. Primeiro iteradores para o primeiro e o último item do container de origem. Depois um iterador para o começo do iterador de destino, seguido pela função que aceita dois argumentos.
Note um detalhe. O container de destino deve estar pronto para receber o resultado do std::transform. O algoritmo não vai nem alocar memória, nem redimensionar o container destino, por isso é necessário redimensionar o std::list de destino para o tamanho do std::list de origem.
Outro detalhe. A função de incremento utiliza operador ++ pré-fixado. Isso é essencial para que o incremento seja executado antes do return, caso utilizássemos o incremento pós-fixado o incremento seria executado após o return, e não teríamos o resultado esperado.
Transformação com Base em Dois Containers
Mas o std::transform é mais flexível que isso, é possível utilizar o conteúdo de dois containers, fazendo uma operação binária entre o seu conteúdo. A sintaxe é bem parecida, com a adição de um ponteiro para o início do segundo container de origem. Vamos ao exemplo:
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <list>
double fdiscount(double price, int disc) { return price * (1 - (double)disc / 100); }
void fcout(double d) { std::cout << "US$ " << std::fixed << std::setprecision(2) << d << std::endl; }
int main() {
std::list<double> prices;
int discounts[] = {10, 15, 20};
std::list<double> newprices;
prices.push_back(10.5);
prices.push_back(15.8);
prices.push_back(12.4);
newprices.resize(prices.size());
std::transform(
prices.begin(), prices.end(),
discounts, newprices.begin(),
fdiscount);
std::for_each(newprices.begin(), newprices.end(), fcout);
}
E a saída, dessa vez conforme esperado:
US$ 9.45
US$ 13.43
US$ 9.92
Nesse exemplo utilizamos duas peculiaridades, além de exemplificar como o algoritmo funciona em operações binárias, demonstra que o std::transform, e vários outros algoritmos STL, também funcionam com arrays padrão C.
Um cuidado a se tomar. Note o cast (double) ao calcular o novo preço. Como o tipo da variável disc é int, as operações efetuadas vão retornar int, o que vai acabar por eliminar as casas decimais e transformando o desconto em 0. Ao colocar o cast transformamos o valor da variável int em double, e as operações são executadas como double, fazendo com que a conta funcione corretamente. Tente remover o cast e veja o que acontece.