es.davy.ai

Preguntas y respuestas de programación confiables

¿Tienes una pregunta?

Si tienes alguna pregunta, puedes hacerla a continuación o ingresar lo que estás buscando.

¿Deberías pasar un objeto invocable como argumento de destino mediante std::function o mediante una referencia universal?

Considere el siguiente código:

#include <functional>
#include <iostream>

struct S1
{
    S1(const std::function<void()>& func) : func{func} {}

    std::function<void()> func;
};

struct S2
{
    template<typename f="">
    S2(F&& func) : func{std::forward<f>(func)} {}

    std::function<void()> func;
};

int main()
{
    S1 s1{[]{ std::cout << "Se llamó a la función S1\n"; }};
    S2 s2{[]{ std::cout << "Se llamó a la función S2\n"; }};

    s1.func();
    s2.func();

    return 0;
}

Estoy tratando de entender cuáles son los beneficios/desventajas de diseño de cada una de las dos implementaciones de constructor. ¿Existen diferencias de rendimiento entre las dos?

Mi pregunta principal es si se debe aceptar un objeto std::function o una referencia universal a un objeto callable cuando se conoce la firma del callable y se almacena como std::function en la clase.

Según lo que he escuchado, la referencia universal es el enfoque preferido porque proporciona la mejor oportunidad para que el compilador haga la optimización inline.

Sin embargo, puedo pensar en algunas desventajas de usar una referencia universal:
– Puede entrar en conflicto con otros constructores que toman el mismo número de argumentos, por lo que debe protegerse utilizando SFINAE o conceptos de C++20. (Este es un problema de biblioteca, así que no es muy importante).
– Es menos explícito sobre qué tipo de objetos callable espera (también se puede restringir mediante conceptos). (Esto podría considerarse un problema del usuario).
– Como resultado de la desventaja anterior, el punto de error en caso de un objeto callable inadecuado se mueve del sitio de llamada al sitio de inicialización del miembro, lo cual es mucho más difícil de entender. (Definitivamente, este es un problema del usuario).

Supongo que mi pregunta real es:
¿Debería exponerse la naturaleza std::function de la clase en la interfaz del constructor o esto es un detalle de implementación que debe mantenerse oculto?</void()></void()></void()>

Tags:  , ,

Answer

  1. Avatar for davy.ai

    Las dos implementaciones del constructor tienen diferentes beneficios/desventajas de diseño. El primer constructor de S1 toma una referencia constante a lvalue a un objeto std::function y lo guarda en la variable miembro. Este enfoque es adecuado cuando se espera que el objeto de función sobreviva a la instancia de la clase o cuando la clase no necesita modificar el objeto de función. Sin embargo, incurre en una copia o en una construcción innecesaria en caso de argumentos rvalue.

    El segundo constructor de S2 toma una referencia de reenvío y utiliza std::forward para reenviarla al constructor de std::function. Este enfoque es adecuado cuando la clase necesita modificar o mover el objeto de función o cuando la clase quiere evitar copias o construcciones innecesarias. Sin embargo, requiere que el objeto de función sea movible o copiable, y puede causar ambigüedades en la resolución de sobrecarga si hay múltiples constructores con firmas similares.

    En cuanto al rendimiento, el segundo enfoque suele ser más eficiente porque evita copias o movimientos innecesarios. Sin embargo, esto puede depender del caso de uso específico y del tamaño y complejidad del objeto de la función.

    Si se debe aceptar un objeto std::function o una referencia universal a un objeto callable depende del caso de uso específico y de los objetivos de diseño. Exponer la naturaleza std::function de la clase en la interfaz del constructor puede ser útil cuando la clase quiere proporcionar una interfaz clara y explícita para los clientes, y permitirles pasar objetos callable arbitrarios como argumentos. Sin embargo, este enfoque también puede limitar la flexibilidad y el rendimiento de la clase.

    Por otro lado, el uso de una referencia universal permite que la clase acepte varios tipos de objetos callable y potencialmente optimice el rendimiento evitando copias o movimientos innecesarios. Sin embargo, también puede introducir ambigüedad y complicar la interfaz, además de ocultar la naturaleza std::function de la clase. Por lo tanto, el diseño debe considerar los compromisos entre flexibilidad, rendimiento, claridad y simplicidad, dependiendo del caso de uso previsto y la audiencia.

Comments are closed.