Funções x Functores… qual a melhor opção?
Na dúvida entre usar um ponteiro para uma função ou criar um functor?
Antes de mais nada, é preciso deixar claro o que é um functor: um objeto função, também chamado de um functor, é uma construção de programação que permite a um objeto ser invocado ou chamado como se fosse uma função comum, geralmente com a mesma sintaxe. Ou seja, qualquer objeto cuja classe redefina o operador “()” pode ser considerado um functor.
Na STL existem diversas funções (a maioria definidas como templates) que recebem como parâmetro um ponteiro para uma função, que pode ser também um functor. Um exemplo típico é a função for_each, que percorre elementos de uma coleção qualquer aplicando uma determinada função. for_each recebe três parâmetros: dois iteradores, início e fim, e uma função (ou functor) que será chamado para cada elemento da coleção. Segue um exemplo onde é mostrado também o código de uma possível versão da função forEach definida como template.
Código Fonte usando ponteiro para função:
#include <cstdio>
using namespace std;
template <class Itr, class F>
inline void forEach( Itr i, Itr fim, F f ) {
while( i != fim )
f( *i++ );
}
inline void print( int n ) {
printf("%d ", n );
}
int main(int argc, char *argv[])
{
int tab[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
forEach( tab, tab+10, print );
}
|
O programa acima imprime os números que estão no array tab. Note que a função forEach é genérica o suficiente par trabalhar tanto com arrays (ponteiros) como com containers da STL. E embora todas as funções estejam definidas como inline, a função print não será expandida inline dentro de forEach – isso ocorre porque está sendo usado um ponteiro para esta função. Por outro lado a função forEach será expandida inline dentro da função main. Observe o código assembly gerado, com as opções de otimização ligadas (-O3).
Código assembly gerado:
.file "main.cpp" .section .rdata,"dr" LC0: .ascii "%d \0" .text .align 2 .p2align 4,,15 .globl __Z5printi .def __Z5printi; .scl 2; .type 32; .endef __Z5printi: pushl %ebp movl %esp, %ebp subl $8, %esp movl 8(%ebp), %eax movl $LC0, (%esp) movl %eax, 4(%esp) call _printf leave ret .def ___main; .scl 2; .type 32; .endef .align 2 .p2align 4,,15 .globl _main .def _main; .scl 2; .type 32; .endef _main: pushl %ebp movl $16, %eax movl %esp, %ebp pushl %edi pushl %esi pushl %ebx subl $76, %esp andl $-16, %esp call __alloca movl $__Z5printi, %edi leal -72(%ebp), %ebx leal -32(%ebp), %esi call ___main movl $0, -72(%ebp) movl $1, -68(%ebp) movl $2, -64(%ebp) movl $3, -60(%ebp) movl $4, -56(%ebp) movl $5, -52(%ebp) movl $6, -48(%ebp) movl $7, -44(%ebp) movl $8, -40(%ebp) movl $9, -36(%ebp) jmp L11 .p2align 4,,7 L13: movl (%ebx), %edx addl $4, %ebx movl %edx, (%esp) call *%edi L11: cmpl %esi, %ebx jne L13 leal -12(%ebp), %esp xorl %eax, %eax popl %ebx popl %esi popl %edi leave ret .def _printf; .scl 2; .type 32; .endef |
Detalhes:
Assembly é um porre de se olhar, mas vamos lá… Coloquei em vermelho as partes importantes. A função __Z5printi é como o compilador chamou a função print do programa fonte em C++. Ela é criada, apesar de ser declarada como inline, porque na função main o seu endereço foi passado para a função forEach, como dito acima. Sempre que for necessário, o compilador irá gerar código para uma função declarada inline, por exemplo quando um ponteiro para a função é usado. Mas não se preocupe, a função não deixou de ser inline, nas outras chamadas ela será expandida normalmente. No código dessa função vemos a chamada a printf. __Z5printi é movido para o registrador edi, e logo em seguida no bloco L13 há um call para o endereço nesse registrador.
Agora compare o que ocorre quando usamos um functor no lugar de uma função. Esse functor é bem simples, apenas uma struct com o operador “()” redefinido, e recebendo um int.
Código Fonte usando functor:
#include <cstdio>
using namespace std;
template <class Itr, class F>
inline void forEach( Itr i, Itr fim, F f ) {
while( i != fim )
f( *i++ );
}
struct Imprime {
void operator () ( int n ) {
printf("%d ", n );
}
};
int main(int argc, char *argv[])
{
int tab[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
forEach( tab, tab+10, Imprime() );
}
|
Note que na chamada a forEach é passado um objeto temporário do tipo Imprime. Pode-se também declarar uma variável local e passá-la, mas não há necessidade. Por outro lado, o C++ exige que ao se criar um objeto temporário se chame o construtor, nesse caso, o default, sem nenhum parâmetro.
Código assembly gerado:
.file "main.cpp" .def ___main; .scl 2; .type 32; .endef .section .rdata,"dr" LC0: .ascii "%d \0" .text .align 2 .p2align 4,,15 .globl _main .def _main; .scl 2; .type 32; .endef _main: pushl %ebp movl $16, %eax movl %esp, %ebp pushl %esi pushl %ebx subl $64, %esp andl $-16, %esp call __alloca leal -56(%ebp), %ebx leal -16(%ebp), %esi call ___main movl $0, -56(%ebp) movl $1, -52(%ebp) movl $2, -48(%ebp) movl $3, -44(%ebp) movl $4, -40(%ebp) movl $5, -36(%ebp) movl $6, -32(%ebp) movl $7, -28(%ebp) movl $8, -24(%ebp) movl $9, -20(%ebp) jmp L11 .p2align 4,,7 L13: movl (%ebx), %edx addl $4, %ebx movl $LC0, (%esp) movl %edx, 4(%esp) call _printf L11: cmpl %esi, %ebx jne L13 leal -8(%ebp), %esp xorl %eax, %eax popl %ebx popl %esi leave ret .def _printf; .scl 2; .type 32; .endef |
Resumo da ópera:
Agora sim… o compilador não declarou nenhuma função para o operador “()” da struct Imprime, expandido a chamada a printf diretamente no código da função main no bloco L13. Portanto, é mais vantajoso usar um functor do que um ponteiro para uma função, especialmente no caso de templates expandidas inline, que são a grande maioria na prática. O código gerado é menor e se evita uma chamada de função. Claro, em um programa pequeno esse custo é desprezível, mas o objetivo aqui é mostrar que o uso de functores geram um código mais eficiente do que o uso de ponteiros para funções, ao contrário do que o senso comum diz. Novamente, é necessário ajudar o otimizador.
Haveria alguma vantagem em usar um functor se o código da template forEach não fosse inline? Sim, pois mesmo dessa forma uma chamada de função é evitada, pois o operador “()” seria expandido inline dentro de forEach.
E se a função forEach não fosse template? Aí, provavelmente ela receberia um functor por referência e polimórfico, ou seja, com o operador “()” declarado como virtual e toda uma hierarquia de functores… e então talvez empatasse com a opção usando ponteiro para função! Mas eficiência não é a única vantagem dos functores: por serem objetos, podem ter outros campos etc.
“Though his mind is not for rent/Don’t put him down as arrogant“
vc ja ouviu falar de functionoid ?
vi isto neste site
http://parashift.com/c++-faq-lite/pointers-to-members.html#faq-33.10
mas não entendi mt bem … vc poderia fazer um post falando a diferença entre functinoid e functor ?
seu blog é 10!
até
Anotado!
Quando voltar das férias escrevo sobre isso.
Abraços!