Является ли преобразование метода C ++ в функцию C с аргументом указателя приемлемым шаблоном?

16

Я использую C ++ на ESP-32. При регистрации таймера я должен сделать это:

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

Здесь таймер звонит soundCallback.

И то же самое при регистрации задачи:

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

Таким образом, метод запускается в отдельной задаче.

GCC всегда предупреждает меня об этих преобразованиях, но работает так, как и планировалось.

Это приемлемо в производственном коде? Есть лучший способ это сделать?

Валь говорит восстановить Монику
источник

Ответы:

47

А reinterpret_castвсегда подозрительно, если не знаешь точно , что делаете. Здесь ваш код работает только из-за соглашения о вызовах GCC для методов C ++, но это сильно пахнет как неопределенное поведение. В частности, вы не должны предполагать, что функции-члены каким-либо образом совместимы с обычными указателями на функции.

Обычный подход состоит в том, чтобы вместо этого определить C-совместимую функцию с соответствующей сигнатурой, которая внутренне вызывает метод C ++. Например:

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

Это приведение в порядке, потому что мы приводим от void*типа к указанному объекту.

Детали:

  • extern "C"определяет языковую связь этой функции. Языковая связь влияет на искажение имени и соглашение о вызовах функции. Функции-члены не могут иметь связи на языке Си. Языковая связь в значительной степени ортогональна внутренней / внешней связи.

  • Для обратного вызова функция может быть «частной», то есть иметь внутреннюю связь. Код C никогда не ссылается на обратный вызов по имени. Приведенный выше фрагмент кода определяет внутреннюю связь через staticключевое слово (не статический метод!). В качестве альтернативы, функция могла бы быть помещена в анонимное пространство имен.

    Я не совсем уверен относительно взаимодействия между extern "C"и static(внутренняя связь). Например , [dcl.link]говорит , что «Все типы функций, имена функций с внешним связыванием, и имена переменных с внешним связыванием имеют языковую связь.» Я расцениваю это так , что тип из my_timer_callbackимеет языковую связь C, но его функция имя не делает.

  • Здесь static_castуместно использовать A , потому что мы знаем реальный тип, argно не можем выразить его в системе типов. Напротив, reinterpret_castуместно, когда мы хотим переосмыслить битовую комбинацию, например указатель на числовой тип.

  • Функции не являются обычными объектами, а функции-члены тем более. Вы можете переинтерпретировать приведение между типами указателей на функции, если функция вызывается только через ее действительный тип (и аналогично для указателей на функции-члены). Можно ли привести указатели функций к другим типам (например, указателям на объекты или указателям на пустоту), зависит от реализации ( фон ). На POSIX void*приводятся между указателями на функции и разрешено, так что dlsym()может работать. Другие приведения, включающие указатели функций (членов), не определены. В частности, приведение между функциями-членами и указателями на функции невозможно.

Амон
источник
1
std::bindТакже не принимает указатель объекта в качестве первого аргумента метода?
говорит Вэл Восстановить Монику
5
@val Да, но это не значит, что функции-члены совместимы с обычными функциями, просто bind () использует алгоритм INVOKE, который обрабатывает функции-члены как отдельный случай от обычных объектов функций, в том числе. функциональные указатели. Поскольку std :: bind () создает функтор, он не подходит для взаимодействия с C.
amon
1
Еще один вопрос: зачем мне extern "C"здесь? Важна ли в этом случае связь С?
говорит Вэл: восстановите Монику
5
@val Если вы хотите иметь возможность вызывать эту функцию из C, она должна использовать соглашение о вызовах C. Это можно сделать, объявив эту функцию с привязкой к языку C, или с помощью специфичных для компилятора расширений (например __attribute__((cdecl)), но не делайте этого). В противном случае функция C ++ не будет иметь C-совместимого соглашения о вызовах (хотя в GCC это обычно работает нормально).
Амон
4
@val Подробнее о том, почему extern "C"это формально необходимо, см. [dcl.link]«Два типа функций с различными языковыми связями являются разными типами, даже если они в остальном идентичны». и [expr.call]«Вызов функции через выражение, тип функции которого отличается от типа функции определения вызываемой функции, приводит к неопределенному поведению»
Бен Фойгт,
-1

Лично, самый совместимый, простой в реализации и понятный подход, который я нашел, - это просто предоставить функцию-обертку, совместимую с ожидаемым интерфейсом C, которая внутренне вызывает метод (и, в случае, если он не статичен, создать экземпляр или использовать существующий экземпляр для этого). Это можно рассматривать как разновидность шаблона проектирования адаптера.

Хесус Алонсо Абад
источник
6
Разве это не то, что ответил Амон?
Дронц
1
@Dronz после второго чтения, да, в основном это так. Как только я прочитал, staticя увидел его как метод и по какой-то причине не осознал, что он не передает thisуказатель в качестве первого аргумента (и последующие дебаты об использовании его std::bindусиливают). Но да, вы абсолютно правы! (Извините за двойной ответ!)
Иисус Алонсо Абад
3
Да, staticимеет как минимум три разных значения. И вы их перепутаете, если не будете осторожны. Я бы сказал, что действительно полезно понимать различия между различными видами использования static, поскольку каждый из них является отличным инструментом сам по себе.
cmaster - восстановить монику