Usando conceitos C++20 para evitar std::function

Aug 22 2020

No passado, quando eu queria um retorno de chamada como parâmetro de função, geralmente decidia usar std::function. Em casos raros em que definitivamente nunca uso capturas, usei um typedefpara uma declaração de função.

Portanto, geralmente minha declaração com um parâmetro de retorno de chamada é mais ou menos assim:

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

No entanto, até onde eu sei, std::functionestá realmente dando um pouco de trabalho em tempo de execução devido a ter que resolver o lambda com suas capturas para o std::functionmodelo e mover/copiar suas capturas (?).

Lendo sobre os novos recursos do C++ 20, percebi que poderia usar conceitos para evitar o uso std::functione usar um parâmetro restrito para qualquer functor viável.

E é aqui que surge o meu problema: como quero trabalhar com os objetos do functor de retorno de chamada em algum momento no futuro, preciso armazená-los. Como não tenho um tipo definitivo para meu retorno de chamada, meu pensamento inicial foi copiar (eventualmente mover em algum ponto) o functor para empilhar e usar um std::vector<void*>para anotar onde os deixei.

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
                   });

  // [...]
}

Embora isso funcione bem o suficiente, ao implementar o método que deveria chamar o functor, notei que acabei de adiar o problema do tipo desconhecido/ausente. Pelo que entendi, lançar a void*para um ponteiro de função ou algum hack semelhante deve resultar em UB - Como o compilador saberia que estou realmente tentando chamar operator () de uma classe que é completamente desconhecida?

Pensei em armazenar o functor (copiado) junto com o ponteiro da função para sua operator()definição, porém não faço ideia de como poderia injetar o functor como thisdentro da função, e sem ele duvido que captures funcionariam.

Outra abordagem que tive foi declarar uma interface virtual pura que declare a operator()função necessária. Infelizmente, meu compilador me proíbe de lançar meu functor para a interface e também não acho que haja uma maneira legal de deixar o lambda derivar dele.

Então, existe uma maneira de resolver isso ou possivelmente estou apenas usando mal o recurso de requisitos/conceitos do modelo?

Respostas

13 NicolBolas Aug 22 2020 at 00:35

Sua versão inicial usada std::functionjustamente porque apaga o tipo. Se você deseja o apagamento de tipo (e claramente deseja, já que deseja que o usuário possa usar qualquer tipo sem que seu código saiba explicitamente qual é esse tipo), então você precisa de alguma forma de apagamento de tipo. E o apagamento de tipo não é gratuito.

As restrições são para modelos. Você não quer uma função de modelo; você deseja uma única função que lide com um callable apagado por tipo.

E para retornos de chamada que precisam sobreviver à pilha de chamadas do provedor, std::functiona sobrecarga de é praticamente o que você precisa. Ou seja, o "overhead" não é inútil; é o que permite que você armazene objetos de tipos desconhecidos e arbitrários em seu processador de retorno de chamada.