No que diz respeito ao padrão C, se você lançar um ponteiro de função para um ponteiro de função de um tipo diferente e depois chamá-lo, é um comportamento indefinido . Consulte o Anexo J.2 (informativo):
O comportamento é indefinido nas seguintes circunstâncias:
- Um ponteiro é usado para chamar uma função cujo tipo não é compatível com o tipo apontado (6.3.2.3).
Seção 6.3.2.3, parágrafo 8 diz:
Um ponteiro para uma função de um tipo pode ser convertido em um ponteiro para uma função de outro tipo e vice-versa; o resultado deve ser igual ao ponteiro original. Se um ponteiro convertido é usado para chamar uma função cujo tipo não é compatível com o tipo apontado, o comportamento é indefinido.
Em outras palavras, você pode lançar um ponteiro de função para um tipo de ponteiro de função diferente, convertê-lo novamente e chamá-lo, e tudo vai funcionar.
A definição de compatível é um tanto complicada. Pode ser encontrado na seção 6.7.5.3, parágrafo 15:
Para que dois tipos de função sejam compatíveis, ambos devem especificar os tipos de retorno compatíveis 127 .
Além disso, as listas de tipo de parâmetro, se ambas estiverem presentes, devem concordar no número de parâmetros e no uso do terminador de reticências; os parâmetros correspondentes devem ter tipos compatíveis. Se um tipo tem uma lista de tipo de parâmetro e o outro tipo é especificado por um declarador de função que não faz parte de uma definição de função e que contém uma lista de identificadores vazia, a lista de parâmetros não deve ter um terminador de reticências e o tipo de cada parâmetro deve ser compatível com o tipo que resulta da aplicação das promoções de argumento padrão. Se um tipo tem uma lista de tipo de parâmetro e o outro tipo é especificado por uma definição de função que contém uma lista de identificadores (possivelmente vazia), ambos devem concordar no número de parâmetros, e o tipo de cada parâmetro de protótipo deve ser compatível com o tipo que resulta da aplicação das promoções do argumento padrão ao tipo do identificador correspondente. (Na determinação da compatibilidade de tipo e de um tipo composto, cada parâmetro declarado com tipo de função ou array é considerado como tendo o tipo ajustado e cada parâmetro declarado com tipo qualificado é considerado como tendo a versão não qualificada de seu tipo declarado.)
127) Se ambos os tipos de função forem do '' estilo antigo '', os tipos de parâmetro não são comparados.
As regras para determinar se dois tipos são compatíveis estão descritas na seção 6.2.7 e não as citarei aqui, pois são bastante extensas, mas você pode lê-las no rascunho do padrão C99 (PDF) .
A regra relevante aqui está na seção 6.7.5.1, parágrafo 2:
Para que dois tipos de ponteiros sejam compatíveis, ambos devem ser qualificados de forma idêntica e ambos devem ser ponteiros para tipos compatíveis.
Portanto, como a void*
não é compatível com a struct my_struct*
, um ponteiro de função do tipo void (*)(void*)
não é compatível com um ponteiro de função do tipo void (*)(struct my_struct*)
, portanto, essa conversão de ponteiros de função é tecnicamente um comportamento indefinido.
Na prática, entretanto, você pode se safar com segurança lançando ponteiros de função em alguns casos. Na convenção de chamada x86, os argumentos são colocados na pilha e todos os ponteiros têm o mesmo tamanho (4 bytes em x86 ou 8 bytes em x86_64). Chamar um ponteiro de função se resume a empurrar os argumentos na pilha e fazer um salto indireto para o destino do ponteiro de função e, obviamente, não há noção de tipos no nível do código de máquina.
Coisas que você definitivamente não pode fazer:
- Converta entre ponteiros de função de diferentes convenções de chamada. Você bagunçará a pilha e, na melhor das hipóteses, travará; na pior, terá sucesso silenciosamente com uma enorme brecha de segurança. Na programação do Windows, você costuma passar ponteiros de função. Win32 espera que todas as funções de retorno de chamada para usar a
stdcall
convenção de chamada (que as macros CALLBACK
, PASCAL
e WINAPI
todos expandir a). Se você passar um ponteiro de função que usa a convenção de chamada C padrão ( cdecl
), o resultado será ruim.
- Em C ++, converta entre ponteiros de função de membro de classe e ponteiros de função regular. Isso muitas vezes atrapalha novatos em C ++. As funções de membro de classe têm um
this
parâmetro oculto e, se você converter uma função de membro em uma função regular, não haverá nenhum this
objeto a ser usado e, novamente, muita maldade resultará.
Outra má ideia que às vezes pode funcionar, mas também é um comportamento indefinido:
- Conversão entre ponteiros de função e ponteiros regulares (por exemplo, conversão de a
void (*)(void)
para a void*
). Ponteiros de função não são necessariamente do mesmo tamanho que ponteiros regulares, pois em algumas arquiteturas eles podem conter informações contextuais extras. Isso provavelmente funcionará bem no x86, mas lembre-se de que é um comportamento indefinido.