Использование концепций C ++ 20, чтобы избежать std :: function

Aug 22 2020

Раньше, когда мне требовался обратный вызов в качестве параметра функции, я обычно решал использовать std::function. В редких случаях, когда я определенно никогда не использую захват, я использовал typedefвместо этого для объявления функции.

Итак, обычно мое объявление с параметром обратного вызова выглядит примерно так:

struct Socket
{
  void on_receive(std::function<void(uint8_t*, unsigned long)> cb);
}

Однако, насколько я знаю, std::functionна самом деле выполняет небольшую работу во время выполнения из-за необходимости разрешать лямбда с его захватами в std::functionшаблон и перемещать / копировать его захваты (?).

Читая о новых возможностях C ++ 20, я решил, что смогу использовать концепции, позволяющие избежать std::functionиспользования ограниченного параметра для любого жизнеспособного функтора.

И здесь возникает моя проблема: поскольку я хочу работать с объектами функтора обратного вызова когда-нибудь в будущем, я должен их сохранить. Поскольку у меня нет определенного типа для моего обратного вызова, моей первоначальной мыслью было скопировать (в какой-то момент переместить) функтор в кучу и использовать a, std::vector<void*>чтобы отметить, где я их оставил.

template<typename Functor>
concept ReceiveCallback = std::is_invocable_v<Functor, uint8_t*, unsigned long>
                       && std::is_same_v<typename std::invoke_result<Functor, uint8_t*, unsigned long>::type, void>
                       && std::is_copy_constructible_v<Functor>;
struct Socket
{
  std::vector<void*> callbacks;

  template<ReceiveCallback TCallback>
  void on_receive(TCallback const& callback)
  {
    callbacks.push_back(new TCallback(callback));
  }
}

int main(int argc, char** argv)
{
  Socket* sock;
  // [...] inialize socket somehow

  sock->on_receive([](uint8_t* data, unsigned long length)
                   {
                     // NOP for now
                   });

  // [...]
}

Хотя это работает достаточно хорошо, при реализации метода, который должен вызывать функтор, я заметил, что только что отложил проблему неизвестного / отсутствующего типа. Насколько я понимаю, приведение a void*к указателю на функцию или какой-либо подобный взлом должен дать UB - как компилятор узнает, что я на самом деле пытаюсь вызвать operator () полностью неизвестного класса?

Я думал о сохранении (скопированного) функтора вместе с указателем функции на его operator()определение, однако я понятия не имею, как я могу ввести функтор как thisвнутри функции, и без этого я сомневаюсь, что захваты будут работать.

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

Итак, есть ли способ решить эту проблему, или я, возможно, просто неправильно использую функцию требований / концепций шаблона?

Ответы

13 NicolBolas Aug 22 2020 at 00:35

Ваша первоначальная версия использовалась std::functionименно потому, что стирает шрифт. Если вам нужно стирание типа (и вы явно это делаете, поскольку вы хотите, чтобы пользователь мог использовать любой тип, не зная явно, что это за тип), тогда вам понадобится некоторая форма стирания типа. И стирание шрифтов не бесплатное.

Ограничения для шаблонов. Вам не нужна функция шаблона; вам нужна единственная функция, которая имеет дело с вызываемым объектом со стиранием типа.

А для обратных вызовов, которые должны пережить стек вызовов поставщика, std::functionнакладные расходы - это в значительной степени то, что вам нужно. То есть «накладные расходы» не бессмысленны; это то, что позволяет вам хранить объекты произвольных неизвестных типов в вашем процессоре обратного вызова.