Pergunta

Vamos dizer que eu tenho uma função que aceita um ponteiro de função void (*)(void*) para uso como um callback:

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 esta questão e eu olhei alguns padrões C que dizem que você pode lançar a 'ponteiros de função compatíveis', mas não consigo encontrar uma definição do que significa 'compatível ponteiro de função'.

Foi útil?

Solução

Quanto ao padrão C está em causa, se você lançar um ponteiro de função para um ponteiro de função de um tipo diferente e, em seguida, chamar que, é comportamento indefinido . Ver 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 apontado para Tipo (6.3.2.3).

Secção 6.3.2.3, parágrafo 8 lê:

Um ponteiro para uma função de um tipo pode ser convertida para um apontador para uma função de uma outra digite e vice-versa; o resultado deve comparar igual ao ponteiro originais. Se um convertido apontador são utilizados para chamar uma função cujo tipo não é compatível com o aguçado-digitar, o comportamento é indefinido.

Assim, em outras palavras, você pode lançar um ponteiro de função para um tipo de ponteiro de função diferente, lançá-lo de volta, e chamá-lo, e as coisas vão funcionar.

A definição de compatível é um pouco complicado. Ela pode ser encontrada na seção 6.7.5.3, parágrafo 15:

Por dois tipos de função para ser compatível, ambos devem especificar os tipos de retorno compatíveis 127 .

Além disso, as listas tipo de parâmetro, se ambos estiverem presentes, devem acordar no número de parâmetros e no uso do terminador da elipse; parâmetros correspondentes devem ter tipos compatíveis. Se um tipo tem uma lista tipo de parâmetro eo outro tipo é especificado por um Declarador função que não é parte de uma definição de função e que contém um vazio lista identificador, 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 do default promoções de argumento. Se um tipo tem uma lista tipo de parâmetro eo outro tipo é especificado por uma definição de função que contém uma lista (possivelmente vazio) identificador, ambos devem concordam no número de parâmetros eo tipo de cada parâmetro protótipo será compatível com o tipo que resulta da aplicação do argumento padrão promoções para o tipo do identificador correspondente. (Na determinação do tipo compatibilidade e de um tipo composto, cada parâmetro declarado com função ou matriz tipo é tomada como tendo o tipo ajustado e cada parâmetro declarado com o tipo qualificado é tomada como tendo a versão incondicional do seu tipo declarado.)

127) Se ambos os tipos de função são ‘‘velho estilo’’, tipos de parâmetros não são comparados.

As regras para determinar se dois tipos são compatíveis são descritos na seção 6.2.7, e não vou citá-los aqui já que eles são bastante longa, mas você pode lê-los no projecto do padrão C99 (PDF) .

A regra relevante aqui é na seção 6.7.5.1, parágrafo 2:

Por dois tipos de ponteiro para ser compatível, ambas serão identicamente qualificado e ambas serão ponteiros para tipos compatíveis.

Assim, uma vez que um void* não é compatível com um 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*), de modo que este elenco de ponteiros de função é tecnicamente um comportamento indefinido.

Na prática, porém, você pode seguramente fugir com vazamento ponteiros de função em alguns casos. Na convenção x86 chamando, os argumentos são colocados na pilha, e todos os ponteiros são do 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 que faz um salto indireta para a funçãoalvo ponteiro, e há obviamente nenhuma noção de tipos no nível de código de máquina.

As coisas que você definitivamente não pode fazer:

  • Elenco entre ponteiros de função de diferentes convenções de chamada. Você vai estragar a pilha e na melhor das hipóteses, acidente, na pior das hipóteses, ter sucesso em silêncio com um enorme furo de segurança. Na programação do Windows, muitas vezes você passar ponteiros de função ao redor. Win32 espera que todas as funções de retorno de chamada para usar a convenção stdcall chamando (o que as macros CALLBACK, PASCAL e WINAPI todos expandir a). Se você passar um ponteiro de função que usa a convenção C de chamada padrão (cdecl), maldade irá resultar.
  • Em C ++, fundido entre ponteiros de função de membro de classe e ponteiros de função regulares. Isso muitas vezes viagens até novatos C ++. funções de membro de classe tem um parâmetro this escondido, e se você lançar uma função de membro para uma função regular, não há nenhum objeto this para uso, e novamente, muito maldade irá resultar.

Outra má idéia que pode, por vezes trabalho, mas também é um comportamento indefinido:

  • Fundição entre ponteiros de função e ponteiros regulares (por exemplo, lançando um void (*)(void) a um void*). ponteiros de função não são necessariamente do mesmo tamanho como ponteiros regulares, já que em algumas arquiteturas que pode conter informação extra contextual. Este, provavelmente, ok trabalho em x86, mas lembre-se que é um comportamento indefinido.

Outras dicas

Eu perguntei sobre exata esta mesma questão sobre algum código em GLib recentemente. (GLib é uma biblioteca central para o projeto GNOME e escrito em C.) foi-me dito os slots'n'signals inteiras quadro depende disso.

Durante todo o código, há numerosos exemplos de transmissão a partir do tipo (1) para (2):

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

É comum cadeia-thru com chamadas como este:

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 por si mesmo aqui em g_array_sort(): http: //git.gnome .org / browse / glib / árvore / glib / garray.c

As respostas acima são detalhados e provavelmente corretas - se você se sentar no comitê de padrões. Adam e Johannes merecem crédito por suas respostas bem pesquisadas. No entanto, em estado selvagem, você vai encontrar este código funciona muito bem. Controverso? Sim. Considere o seguinte: compila GLib / trabalhos / testes em um grande número de plataformas (Linux / Solaris / Windows / OS X) com uma grande variedade de compiladores / ligadores / carregadores de kernel (GCC / Clang / MSVC). Padrões que se dane, eu acho.

Eu passei algum tempo pensando sobre essas respostas. Aqui é a minha conclusão:

  1. Se você estiver escrevendo uma biblioteca de retorno de chamada, isso pode ser OK. Caveat.emptor -. Uso em seu próprio risco
  2. Else, não fazê-lo.

Pensar mais profundo depois de escrever essa resposta, eu não ficaria surpreso se o código para compiladores C usa esse mesmo truque. E desde que (a maioria / todos?) Modernos compiladores C são bootstrapped, isso implicaria o truque é seguro.

A mais importante questão a pesquisa: alguém pode encontrar uma plataforma / compilador / vinculador / carregador onde este truque faz não trabalho? Principais pontos de brownie para que um. Aposto que existem alguns processadores / sistemas embarcados que não gosto. No entanto, para o desktop de computação (e provavelmente / tablet móvel), este truque provavelmente ainda funciona.

O ponto 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ó irá gerar o código para my_callback_helper se for realmente necessário, caso em que você seria feliz que fez.

Você tem um tipo de função compatíveis se o tipo de retorno e parâmetro tipos são compatíveis - basicamente (É mais complicado, na realidade :)). A compatibilidade é o mesmo que "mesmo tipo" só mais frouxa para permitir ter tipos diferentes, mas ainda têm alguma forma de dizer "estes tipos são quase os mesmos". Em C89, por exemplo, duas estruturas eram compatíveis se fossem de outro modo idêntico, mas apenas seu nome era diferente. C99 parece ter mudado isso. Citando o c lógica documento (leitura altamente recomendado, aliás!):

Estrutura, declarações de união, ou tipo de enumeração em duas unidades de tradução diferentes não declarar formalmente do mesmo tipo, mesmo que o texto dessas declarações vêm do mesmo arquivo de inclusão, uma vez que as unidades de tradução são eles próprios disjuntos. A norma especifica, assim, as regras de compatibilidade adicionais para esses tipos, de modo que se dois tais declarações são suficientemente semelhantes eles são compatíveis.

Dito isto - sim estritamente este é um comportamento indefinido, porque a sua função do_stuff ou alguém vai chamar sua função com um ponteiro de função tendo void* como parâmetro, mas a sua função tem um parâmetro incompatível. Mas, no entanto, espero que todos os compiladores para compilar e executá-lo sem gemer. Mas você pode fazer mais limpa por ter outra função tomando um void* (e registrar que, como função de retorno) que só vai chamar sua função real então.

Como C compila o código para instrução que não se importam em tudo sobre tipos de ponteiro, é muito bom para usar o código que você menciona. Você iria ter problemas quando você executar do_stuff com a sua função de retorno e ponteiro para outra coisa estrutura, em seguida, my_struct como argumento.

Espero que eu possa torná-lo mais clara, mostrando que não iria funcionar:

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 converter ponteiros para o que quiser, desde que os dados continuam a fazer sentido em tempo de execução.

Se você pensar sobre a forma como chamadas de função de trabalho em C / C ++, eles empurram certos itens na pilha, salto para o novo local de código, executar, em seguida, pop a pilha no retorno. Se os seus ponteiros de função descrever funções com o mesmo tipo de retorno eo mesmo número / tamanho de argumentos, você deve estar bem.

Assim, eu acho que você deve ser capaz de fazê-lo com segurança.

ponteiros void são compatíveis com outros tipos de ponteiro. É a espinha dorsal de como malloc e as funções MEM (memcpy, memcmp) trabalho. Tipicamente, em C (Em vez de C ++) NULL é uma macro definido como ((void *)0).

Olhe para 6.3.2.3 (item 1) no C99:

Um ponteiro de vazio pode ser convertido em ou a partir de um ponteiro para qualquer tipo incompleto ou objecto

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top