STL Algorithms: Alteração de Containers com std::transform

por Fabio A. Mazzarino

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.