Uso de conceptos de C++20 para evitar std::function

Aug 22 2020

En el pasado, cuando quería una devolución de llamada como parámetro de función, generalmente decidí usar std::function. En casos raros en los que definitivamente nunca uso capturas, en su lugar usé a typedefpara una declaración de función.

Entonces, por lo general, mi declaración con un parámetro de devolución de llamada se parece a esto:

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

Sin embargo, hasta donde yo sé, std::functionen realidad está haciendo un poco de trabajo en tiempo de ejecución debido a que tiene que resolver el lambda con sus capturas en la std::functionplantilla y mover/copiar sus capturas (?).

Al leer sobre las nuevas funciones de C++ 20, pensé que podría utilizar conceptos para evitar el std::functionuso de un parámetro restringido para cualquier funtor viable.

Y aquí es donde surge mi problema: como quiero trabajar con los objetos del funtor de devolución de llamada en el futuro, tengo que almacenarlos. Dado que no tengo un tipo definitivo para mi devolución de llamada, mi pensamiento inicial fue copiar (eventualmente mover en algún momento) el functor para acumular y usar a std::vector<void*>para anotar dónde los dejé.

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

  // [...]
}

Si bien esto funciona lo suficientemente bien, al implementar el método que se supone que debe llamar al funtor, noté que acabo de posponer el problema del tipo desconocido/faltante. Según tengo entendido, enviar un void*puntero a una función o algún truco similar debería producir UB. ¿Cómo sabría el compilador que en realidad estoy tratando de llamar al operador () de una clase que es completamente desconocida?

Pensé en almacenar el funtor (copiado) junto con el puntero de función a su operator()definición, sin embargo, no tengo idea de cómo podría inyectar el funtor thisdentro de la función, y sin él, dudo que las capturas funcionen.

Otro enfoque que tuve fue declarar una interfaz virtual pura que declara la operator()función requerida. Desafortunadamente, mi compilador me prohíbe enviar mi funtor a la interfaz y tampoco creo que haya una forma legal de dejar que la lambda se derive de él.

Entonces, ¿hay alguna manera de resolver esto o posiblemente estoy haciendo un mal uso de la función de requisitos/conceptos de la plantilla?

Respuestas

13 NicolBolas Aug 22 2020 at 00:35

Su versión inicial utilizada std::functionprecisamente porque borra el tipo. Si desea borrar el tipo (y claramente lo desea, ya que desea que el usuario pueda usar cualquier tipo sin que su código sepa explícitamente cuál es ese tipo), entonces necesita alguna forma de borrado de tipo. Y el borrado de tipos no es gratis.

Las restricciones son para las plantillas. No desea una función de plantilla; desea una sola función que se ocupe de un invocable de tipo borrado.

Y para las devoluciones de llamadas que tienen que sobrevivir a la pila de llamadas del proveedor, std::functionla sobrecarga de es más o menos lo que necesita. Es decir, los "gastos generales" no tienen sentido; es lo que le permite almacenar objetos de tipos arbitrarios y desconocidos en su procesador de devolución de llamada.