Czy konwersja metody C ++ na funkcję C z argumentem wskaźnika jest akceptowalnym wzorcem?

16

Używam C ++ na ESP-32. Przy rejestracji timera muszę to zrobić:

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

Tutaj dzwoni zegar soundCallback.

I to samo podczas rejestracji zadania:

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

Tak więc metoda jest uruchamiana w oddzielnym zadaniu.

GCC zawsze ostrzega mnie przed tymi konwersjami, ale działa tak, jak zaplanowano.

Czy jest to dopuszczalne w kodzie produkcyjnym? Czy jest na to lepszy sposób?

val mówi Przywróć Monikę
źródło

Odpowiedzi:

47

A reinterpret_castzawsze jest podejrzane, chyba że dokładnie wiesz , co robisz. Tutaj twój kod działa tylko z powodu konwencji wywoływania GCC dla metod C ++, ale pachnie to mocno jak niezdefiniowane zachowanie. W szczególności nie należy zakładać, że funkcje składowe są w jakikolwiek sposób kompatybilne z normalnymi wskaźnikami funkcji.

Zazwyczaj zamiast tego należy zdefiniować funkcję kompatybilną z C z odpowiednią sygnaturą, która wewnętrznie wywołuje metodę C ++. Na przykład:

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

Ten rzut jest w porządku, ponieważ rzutujemy z powrotem void*na typ wskazanego obiektu.

Detale:

  • extern "C"określa powiązanie językowe tej funkcji. Powiązanie językowe wpływa na zniekształcanie nazw i konwencję wywoływania funkcji. Funkcje składowe nie mogą mieć powiązania języka C. Powiązanie językowe jest w dużej mierze ortogonalne względem powiązań wewnętrznych / zewnętrznych.

  • W przypadku oddzwaniania funkcja może być „prywatna”, tzn. Mieć wewnętrzne powiązanie. Kod C nigdy nie odwołuje się do wywołania zwrotnego według nazwy. Powyższy fragment kodu określa wewnętrzne powiązanie za pomocą staticsłowa kluczowego (nie metoda statyczna!). Alternatywnie, funkcja mogła zostać umieszczona w anonimowej przestrzeni nazw.

    Nie jestem całkowicie pewien interakcji między extern "C"i static(powiązanie wewnętrzne). Na przykład [dcl.link]mówi, że „wszystkie rodzaje funkcji, nazwy funkcji z zewnętrznym łącznikiem i nazwy zmiennych z zewnętrznym wiązaniem mają powiązania języka.” I to zinterpretować tak, że typ z my_timer_callbackma powiązania języka C, ale że jego funkcja nazwa nie.

  • Odpowiedni static_castjest tutaj, ponieważ znamy prawdziwy typ, argale nie możemy wyrazić go w systemie typów. Natomiast a reinterpret_castjest właściwe, gdy chcemy ponownie zinterpretować wzór bitowy, np. Wskaźnik na typ liczbowy.

  • Funkcje nie są zwykłymi obiektami, a funkcje składowe jeszcze mniej. Możesz reinterpretować rzutowanie między typami wskaźników funkcji, o ile funkcja jest wywoływana tylko przez jej typ rzeczywisty (i analogicznie dla wskaźników funkcji składowych). To, czy możesz rzutować wskaźniki funkcji na inne typy (np. Wskaźniki obiektów lub wskaźniki pustki), jest określone w implementacji (w tle ). W POSIX rzutuje między wskaźnikami funkcji i void*są dozwolone, aby dlsym()działało. Inne rzutowania obejmujące wskaźniki funkcji (składowe) są niezdefiniowane. W szczególności rzutowania między funkcjami elementu i wskaźnikami funkcji nie są możliwe.

amon
źródło
1
Czy nie std::bindzakłada również wskaźnika obiektu jako argumentu pierwszej metody?
val mówi Przywróć Monikę
5
@val Tak, ale to nie znaczy, że funkcje składowe są kompatybilne ze zwykłymi funkcjami, tylko że bind () korzysta z algorytmu INVOKE, który obsługuje funkcje składowe jako osobny przypadek od zwykłych obiektów funkcji, w tym. wskaźniki funkcji. Ponieważ std :: bind () tworzy funktor, nie nadaje się do współpracy z C.
amon
1
Kolejne pytanie: dlaczego extern "C"tu potrzebuję ? Czy połączenie C jest ważne w tym przypadku?
val mówi Przywróć Monikę
5
@val Jeśli chcesz móc wywoływać tę funkcję z C, musi ona używać konwencji wywoływania C. Można to zrobić poprzez zadeklarowanie tej funkcji z łączeniem w języku C lub poprzez rozszerzenia specyficzne dla kompilatora (np. __attribute__((cdecl))Ale nie rób tego). W przeciwnym razie nie można zagwarantować, że funkcja C ++ ma konwencję wywoływania zgodną z C (chociaż w GCC zwykle działa dobrze).
amon,
4
@val Aby uzyskać szczegółowe informacje na temat tego, dlaczego extern "C"jest to formalnie konieczne, zobacz [dcl.link]„Dwa typy funkcji z różnymi powiązaniami językowymi są odrębnymi typami, nawet jeśli poza tym są identyczne”. oraz [expr.call]„Wywołanie funkcji za pomocą wyrażenia, którego typ funkcji różni się od typu funkcji definicji wywoływanej funkcji, powoduje nieokreślone zachowanie”
Ben Voigt
-1

Osobiście najbardziej kompatybilnym, łatwym do wdrożenia i łatwym do zrozumienia podejściem, które znalazłem, jest po prostu zapewnienie funkcji „otoki”, kompatybilnej z oczekiwanym interfejsem C, który wewnętrznie wywołuje metodę (a jeśli nie jest statyczna, utworzyć instancję lub użyć do tego istniejącej instancji). Można to uznać za odmianę Wzorca Projektowania Adaptera.

Jesus Alonso Abad
źródło
6
Czy nie na to odpowiedział amon?
Dronz,
1
@Dronz po drugim czytaniu, tak, to w większości to. Gdy tylko przeczytałem static, zobaczyłem to jako metodę i z jakiegoś powodu nie zdawałem sobie sprawy, że thiswskaźnik nie jest pierwszym argumentem (i następną debatą na temat zastosowania std::bindwzmocnionego go). Ale tak, masz absolutną rację! (Przepraszam za podwójną odpowiedź!)
Jesus Alonso Abad
3
Tak, staticma co najmniej trzy różne, różne znaczenia. I pomieszasz je, jeśli nie będziesz ostrożny. Powiedziałbym, że naprawdę pomocne jest zrozumienie różnic między różnymi zastosowaniami static, ponieważ każde z nich jest doskonałym narzędziem.
cmaster