Digamos que eu tenha uma função que aceita um void (*)(void*)
ponteiro de função para uso como retorno de chamada:
void do_stuff(void (*callback_fp)(void*), void* callback_arg);
Agora, se eu tiver uma função como esta:
void my_callback_function(struct my_struct* arg);
Posso fazer isso com segurança?
do_stuff((void (*)(void*)) &my_callback_function, NULL);
Eu olhei para esta questão e olhei para alguns padrões C que dizem que você pode converter para 'ponteiros de função compatíveis', mas não consigo encontrar uma definição do que significa 'ponteiro de função compatível'.
c
function-pointers
Mike Weller
fonte
fonte
void (*func)(void *)
significa quefunc
é um ponteiro para uma função com uma assinatura de tipo comovoid foo(void *arg)
. Então, sim, você está certo.Respostas:
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):
Seção 6.3.2.3, parágrafo 8 diz:
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:
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:
Portanto, como a
void*
não é compatível com astruct my_struct*
, um ponteiro de função do tipovoid (*)(void*)
não é compatível com um ponteiro de função do tipovoid (*)(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:
stdcall
convenção de chamada (que as macrosCALLBACK
,PASCAL
eWINAPI
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.this
parâmetro oculto e, se você converter uma função de membro em uma função regular, não haverá nenhumthis
objeto a ser usado e, novamente, muita maldade resultará.Outra má ideia que às vezes pode funcionar, mas também é um comportamento indefinido:
void (*)(void)
para avoid*
). 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.fonte
void*
é que eles são compatíveis com qualquer outro ponteiro? Não deve haver nenhum problema em converter de astruct my_struct*
para avoid*
, na verdade, você nem deveria ter que lançar, o compilador deve apenas aceitar. Por exemplo, se você passar astruct my_struct*
para uma função que recebe avoid*
, não é necessário converter. O que estou perdendo aqui que os torna incompatíveis?void*
tipos de ponteiro de função, consulte as especificações .void *
é apenas "compatível com" qualquer outro ponteiro (não funcional) de maneiras definidas com muita precisão (que não estão relacionadas ao que o padrão C significa com a palavra "compatível" neste caso). C permite quevoid *
a seja maior ou menor que astruct my_struct *
, ou tenha os bits em ordem diferente ou negados ou o que for. Portanto,void f(void *)
evoid f(struct my_struct *)
pode ser incompatível com ABI . C irá converter os próprios ponteiros para você, se necessário, mas não irá e às vezes não poderá converter uma função apontada para obter um tipo de argumento possivelmente diferente.Eu perguntei sobre esse mesmo problema em relação a algum código no GLib recentemente. (GLib é uma biblioteca central para o projeto GNOME e escrita em C.) Disseram-me que todo o framework slots'n'signals depende disso.
Em todo o código, existem inúmeras instâncias de conversão do tipo (1) a (2):
typedef int (*CompareFunc) (const void *a, const void *b)
typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)
É comum fazer encadeamento com chamadas como esta:
int stuff_equal (GStuff *a, GStuff *b, CompareFunc compare_func) { return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL); } int stuff_equal_with_data (GStuff *a, GStuff *b, CompareDataFunc compare_func, void *user_data) { int result; /* do some work here */ result = compare_func (data1, data2, user_data); return result; }
Veja você mesmo aqui em
g_array_sort()
: http://git.gnome.org/browse/glib/tree/glib/garray.cAs respostas acima são detalhadas e provavelmente corretas - se você fizer parte do comitê de padrões. Adam e Johannes merecem crédito por suas respostas bem pesquisadas. No entanto, lá fora, você descobrirá que este código funciona muito bem. Controverso? Sim. Considere o seguinte: GLib compila / funciona / testa em um grande número de plataformas (Linux / Solaris / Windows / OS X) com uma ampla variedade de compiladores / linkers / carregadores de kernel (GCC / CLang / MSVC). Padrões que se danem, eu acho.
Passei algum tempo pensando nessas respostas. Aqui está minha conclusão:
Pensando mais profundamente depois de escrever esta resposta, não ficaria surpreso se o código para compiladores C usasse esse mesmo truque. E uma vez que (a maioria / todos?) Os compiladores C modernos são inicializados, isso implicaria que o truque é seguro.
Uma questão mais importante para pesquisar: alguém pode encontrar uma plataforma / compilador / linker / carregador onde esse truque não funcione? Pontos principais de brownie para aquele. Aposto que existem alguns processadores / sistemas embarcados que não gostam. No entanto, para a computação desktop (e provavelmente celular / tablet), esse truque provavelmente ainda funciona.
fonte
A questão realmente não é se você pode. A solução trivial é
void my_callback_function(struct my_struct* arg); void my_callback_helper(void* pv) { my_callback_function((struct my_struct*)pv); } do_stuff(&my_callback_helper);
Um bom compilador só gerará código para my_callback_helper se for realmente necessário, caso em que você ficaria feliz por isso.
fonte
my_callback_helper
, a menos que esteja sempre embutido. Definitivamente, isso não é necessário, pois a única coisa que tende a fazer éjmp my_callback_function
. O compilador provavelmente deseja ter certeza de que os endereços das funções são diferentes, mas infelizmente ele faz isso mesmo quando a função está marcada com C99inline
(ou seja, "não se preocupe com o endereço").void *
pode ser até mesmo de tamanho diferente de astruct *
(acho que está errado, porque de outra formamalloc
estaria quebrado, mas esse comentário tem 5 votos positivos, então estou dando algum crédito. Se @mtraceur estiver certo, a solução que você escreveu não seria correta.void*
ainda tem que funcionar. Resumindo,void*
pode ter mais bits, mas se você converter astruct*
paravoid*
esses bits extras, os bits extras poderão ser zeros e a conversão de volta poderá simplesmente descartar esses zeros novamente.void *
poderia (em teoria) ser tão diferente de umstruct *
. Estou implementando uma vtable em C e usando umthis
ponteiro C ++ - ish como o primeiro argumento para funções virtuais. Obviamente,this
deve ser um ponteiro para a estrutura "atual" (derivada). Então, funções virtuais precisam de protótipos diferentes dependendo da estrutura em que são implementadas. Pensei que usar umvoid *this
argumento resolveria tudo, mas agora aprendi que é um comportamento indefinido ...Você tem um tipo de função compatível se o tipo de retorno e os tipos de parâmetro forem compatíveis - basicamente (é mais complicado na realidade :)). Compatibilidade é o mesmo que "mesmo tipo", apenas mais frouxa para permitir ter tipos diferentes, mas ainda tem alguma forma de dizer "esses tipos são quase iguais". No C89, por exemplo, duas estruturas eram compatíveis se fossem idênticas, mas apenas o nome fosse diferente. C99 parece ter mudado isso. Citando o documento de justificativa c (leitura altamente recomendada, aliás!):
Dito isso - sim, estritamente, este é um comportamento indefinido, porque sua função do_stuff ou outra pessoa irá chamar sua função com um ponteiro de função tendo
void*
como parâmetro, mas sua função tem um parâmetro incompatível. Mesmo assim, espero que todos os compiladores o compilem e executem sem reclamar. Mas você pode fazer mais limpo tendo outra função tomando umavoid*
(e registrando-a como função de retorno de chamada) que apenas chamará sua função real.fonte
Como o código C compila para uma instrução que não se preocupa com os tipos de ponteiro, é muito bom usar o código que você mencionou. Você teria problemas quando executasse do_stuff com sua função de retorno de chamada e apontasse para algo diferente da estrutura my_struct como argumento.
Espero poder deixar isso mais claro, mostrando o que não funcionaria:
int my_number = 14; do_stuff((void (*)(void*)) &my_callback_function, &my_number); // my_callback_function will try to access int as struct my_struct // and go nuts
ou...
void another_callback_function(struct my_struct* arg, int arg2) { something } do_stuff((void (*)(void*)) &another_callback_function, NULL); // another_callback_function will look for non-existing second argument // on the stack and go nuts
Basicamente, você pode lançar ponteiros para o que quiser, contanto que os dados continuem a fazer sentido em tempo de execução.
fonte
Se você pensar sobre como as chamadas de função funcionam em C / C ++, elas colocam certos itens na pilha, saltam para o novo local do código, executam e, em seguida, devolvem a pilha. Se seus ponteiros de função descrevem funções com o mesmo tipo de retorno e o mesmo número / tamanho de argumentos, você deve estar bem.
Portanto, acho que você deve ser capaz de fazer isso com segurança.
fonte
struct
-pointers evoid
-pointers tiverem representações de bits compatíveis; não é garantido que seja o casoOs ponteiros nulos são compatíveis com outros tipos de ponteiros. É a espinha dorsal de como malloc e as funções mem (
memcpy
,memcmp
) funcionam. Normalmente, em C (em vez de C ++)NULL
é uma macro definida como((void *)0)
.Olhe para 6.3.2.3 (Item 1) em C99:
fonte