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?

val diz Reinstate Monica
fonte

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.

amon
fonte
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.

Jesus Alonso Abad
fonte
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