A conversão de um método C ++ em uma função C com um argumento de ponteiro é um padrão aceitável?


16

Eu uso C ++ no ESP-32. Ao registrar um timer, tenho que fazer o seguinte:

timer_args.callback = reinterpret_cast<esp_timer_cb_t>(&SoundMixer::soundCallback);
timer_args.arg = this;

Aqui o timer chama soundCallback.

E a mesma coisa ao registrar uma tarefa:

xTaskCreate(reinterpret_cast<TaskFunction_t>(&SoundProviderTask::taskProviderCode), "SProvTask", stackSize, this, 10, &taskHandle);

Portanto, o método é iniciado em uma tarefa separada.

O GCC sempre me alerta sobre essas conversões, mas funciona da maneira planejada.

É aceitável no código de produção? Existe uma maneira melhor de fazer isso?

Respostas:


47

A reinterpret_casté sempre suspeito, a menos que você saiba exatamente o que está fazendo. Aqui, seu código funciona apenas devido à convenção de chamada do GCC para métodos C ++, mas isso cheira muito a um comportamento indefinido. Em particular, você não deve assumir que as funções de membro sejam de alguma forma compatíveis com os ponteiros de função normais.

A abordagem usual seria definir uma função compatível com C com a assinatura apropriada, que chama internamente o método C ++. Por exemplo:

extern "C" static void my_timer_callback(void* arg) {
  static_cast<SoundMixer*>(arg)->soundCallback();
}

Esse elenco é bom porque estamos retornando de a void*para o tipo de objeto apontado.

Detalhes:

  • extern "C"especifica o vínculo de idioma dessa função. O vínculo de idioma afeta a separação de nomes e a convenção de chamada da função. As funções de membro não podem ter ligação no idioma C. O vínculo linguístico é amplamente ortogonal ao vínculo interno / externo.

  • Para um retorno de chamada, a função pode ser "privada", ou seja, ter ligação interna. O código C nunca se refere ao retorno de chamada pelo nome. O trecho de código acima especifica a ligação interna por meio da staticpalavra - chave (não é um método estático!). Como alternativa, a função poderia ter sido colocada em um espaço para nome anônimo.

    Não tenho muita certeza das interações entre extern "C"e static(ligação interna). Por exemplo, [dcl.link]diz que “todos os tipos de funções, nomes de funções com vínculo externo e nomes de variáveis ​​com vínculo externo têm um vínculo de idioma”. Interpreto isso para que o tipo de my_timer_callbackvínculo de linguagem C seja usado, mas seu nome para a função não.

  • A static_casté apropriado aqui porque conhecemos o tipo real do, argmas não podemos expressá-lo no sistema de tipos. Por outro lado, a reinterpret_casté apropriado quando queremos reinterpretar um padrão de bits, por exemplo, um ponteiro para um tipo numérico.

  • Funções não são objetos comuns e funções-membro ainda menos. Você pode reinterpretar a conversão entre tipos de ponteiros de função, desde que a função seja invocada apenas por meio de seu tipo real (e analogamente para ponteiros de função de membro). A possibilidade de converter ponteiros de função para outros tipos (por exemplo, ponteiros de objeto ou ponteiros nulos) é definida pela implementação (em segundo plano ). No POSIX, projeta entre ponteiros de função e void*é permitido para que dlsym()funcione. Outras conversões envolvendo ponteiros de função (membro) são indefinidas. Em particular, não é possível converter entre funções de membro e ponteiros de função.


1
std::bindTambém não assume o ponteiro de objeto como primeiro argumento de método?
val diz Reintegrar Monica

5
@val Sim, mas isso não significa que as funções de membro sejam compatíveis com funções comuns, apenas que o bind () usa o algoritmo INVOKE, que lida com funções de membro como um caso separado dos objetos de função comum, incl. ponteiros de função. Porque std :: bind () cria um functor ele não é adequado para fazer a interface com C.
amon

1
Outra pergunta: por que eu preciso extern "C"aqui? A ligação C é importante neste caso?
val diz Reintegrar Monica

5
@val Se você deseja chamar essa função de C, ela deve usar a convenção de chamada C. Isso pode ser feito declarando essa função com ligação à linguagem C ou por extensões específicas do compilador (como __attribute__((cdecl)), mas não faça isso). Não é garantido que uma função C ++ tenha uma convenção de chamada compatível com C (caso contrário, no GCC, normalmente funciona bem).
amon

4
@val Para obter detalhes sobre o motivo extern "C"formalmente necessário, consulte [dcl.link]"Dois tipos de funções com diferentes links de idiomas são tipos distintos, mesmo que sejam idênticos." e [expr.call]"Chamar uma função através de uma expressão cujo tipo de função é di ff erent do tipo de função dos resultados fi nição da chamada da função no comportamento definido fi unde"
Ben Voigt

-1

Pessoalmente, a abordagem mais compatível, fácil de implementar e de entender que encontrei é apenas fornecer uma função "wrapper", compatível com a interface C esperada, que chama internamente o método (e, caso não seja estático, instanciar ou usar uma instância existente para fazer isso). Pode ser visto como uma espécie de variação do padrão de design do adaptador.


6
Não foi isso que Amon respondeu?
Dronz 12/08/18

1
@Dronz depois de uma segunda leitura, sim, é principalmente isso. Assim que li static, vi-o como um método e, por algum motivo, não percebi que não passava o thisponteiro como o primeiro argumento (e o debate a seguir sobre o uso do std::bindreforço). Mas sim, você está absolutamente certo! (Desculpem a dupla resposta!)
Jesus Alonso Abad

3
Sim, statictem pelo menos três significados diferentes e distintos. E você os misturará se não for cuidadoso. Eu diria que é realmente útil entender as distinções entre os diferentes usos de static, pois cada um deles é uma ótima ferramenta.
cmaster - restabelece monica 13/08/18
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.