¿Convertir un método C ++ en una función C con un argumento puntero es un patrón aceptable?

16

Yo uso C ++ en ESP-32. Al registrar un temporizador, tengo que hacer esto:

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

Aquí el cronómetro llama soundCallback.

Y lo mismo al registrar una tarea:

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

Entonces el método se inicia en una tarea separada.

GCC siempre me advierte sobre estas conversiones, pero funciona según lo planeado.

¿Es aceptable en el código de producción? ¿Hay una mejor manera de hacer esto?

val dice reinstalar a Monica
fuente

Respuestas:

47

A reinterpret_castsiempre es sospechoso a menos que sepa exactamente lo que está haciendo. Aquí, su código funciona solo debido a la convención de llamadas de GCC para los métodos C ++, pero esto huele mucho a comportamiento indefinido. En particular, no debe suponer que las funciones miembro son de ninguna manera compatibles con los punteros de función normales.

El enfoque habitual sería definir una función compatible con C con la firma apropiada, que internamente llama al método C ++. Por ejemplo:

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

Este lanzamiento está bien porque estamos regresando de void*a al tipo del objeto señalado.

Detalles:

  • extern "C"especifica el enlace de idioma de esta función. La vinculación del idioma afecta el cambio de nombre y la convención de llamadas de de la función. Las funciones de miembro no pueden tener enlace en lenguaje C. La vinculación del lenguaje es en gran medida ortogonal a la vinculación interna / externa.

  • Para una devolución de llamada, la función puede ser "privada", es decir, tener enlace interno. El código C nunca se refiere a la devolución de llamada por nombre. El fragmento de código anterior especifica la vinculación interna a través destatic palabra clave (¡no es un método estático!). Alternativamente, la función podría haberse colocado en un espacio de nombres anónimo.

    No estoy completamente seguro de las interacciones entre extern "C"y static(enlace interno). Por ejemplo, [dcl.link]dice que "Todos los tipos de funciones, nombres de funciones con enlace externo y nombres de variables con enlace externo tienen un enlace de lenguaje". Interpreto esto para que el tipo de my_timer_callbackenlace tenga lenguaje C, pero que su nombre de función no lo .

  • A static_castes apropiado aquí porque conocemos el tipo real de argpero no podemos expresarlo dentro del sistema de tipos. En contraste unreinterpret_cast es apropiado cuando queremos reinterpretar un patrón de bits, por ejemplo, un puntero a un tipo numérico.

  • Las funciones no son objetos ordinarios, y las funciones miembro aún menos. Puede reinterpretar la conversión entre tipos de puntero de función siempre que la función solo se invoque a través de su tipo real (y de forma análoga a los punteros de función miembro). Si puede convertir punteros de función a otros tipos (por ejemplo, punteros de objeto o punteros vacíos) está definido por la implementación ( fondo ). En POSIX, las conversiones entre punteros de función y void*están permitidas para que dlsym()pueda funcionar. Otros modelos que involucran punteros de función (miembro) no están definidos. En particular, las conversiones entre funciones miembro y punteros de función no son posibles.

amon
fuente
1
¿No std::bindsupone también el puntero de objeto como primer argumento de método?
Val dice Reinstate Monica
55
@val Sí, pero eso no significa que las funciones miembro sean compatibles con las funciones ordinarias, simplemente que bind () usa el algoritmo INVOKE que maneja las funciones miembro como un caso separado de los objetos de funciones ordinarias incl. punteros de función. Dado que std :: bind () crea un functor, no es adecuado para interactuar con C.
amon
1
Otra pregunta: ¿por qué necesito extern "C"aquí? ¿Es importante el enlace C en este caso?
Val dice Reinstate Monica
55
@val Si desea poder llamar a esa función desde C, debe usar la convención de llamada C. Esto se puede hacer declarando esa función con enlace de lenguaje C, o mediante extensiones específicas del compilador (como __attribute__((cdecl)), pero no lo haga). De lo contrario, no se garantiza que una función C ++ tenga una convención de llamada compatible con C (aunque en GCC normalmente funciona bien).
amon
44
@val Para obtener detalles sobre por qué extern "C"es formalmente necesario, consulte [dcl.link]"Dos tipos de funciones con diferentes enlaces de idiomas son tipos distintos, incluso si son idénticos". y [expr.call]"Llamar a una función a través de una expresión cuyo tipo de función es diferente del tipo de función de la definición de la función llamada produce un comportamiento indefinido"
Ben Voigt
-1

Personalmente, el enfoque más compatible, fácil de implementar y fácil de entender que he encontrado es proporcionar una función de "envoltura", compatible con la interfaz C esperada, que llama internamente al método (y, en caso de que no sea estático, crear instancias o usar una instancia existente para hacer eso). Podría verse como una especie de variación del patrón de diseño del adaptador.

Jesus Alonso Abad
fuente
66
¿No es eso lo que respondió amon?
Dronz
1
@Dronz después de una segunda lectura, sí, es sobre todo eso. Tan pronto como lo leí, staticlo vi como un método y, por alguna razón, no me di cuenta de que no pasa el thispuntero como primer argumento (y el siguiente debate sobre el uso de std::bindreforzado). Pero sí, tienes toda la razón! (Perdón por la doble respuesta!)
Jesús Alonso Abad
3
Sí, statictiene al menos tres significados diferentes y distintos. Y los mezclará si no tiene cuidado. Diría que es realmente útil comprender las distinciones entre los diferentes usos de static, ya que cada uno es una gran herramienta por derecho propio.
cmaster - reinstalar a monica el