¿Debo pasar una función std :: por const-reference?

141

Digamos que tengo una función que toma un std::function:

void callFunction(std::function<void()> x)
{
    x();
}

¿Debería pasar xpor const-reference en su lugar ?:

void callFunction(const std::function<void()>& x)
{
    x();
}

¿La respuesta a esta pregunta cambia dependiendo de lo que la función haga con ella? Por ejemplo, si se trata de una función miembro de clase o un constructor que almacena o inicializa std::functionen una variable miembro.

Advenimiento de Sven
fuente
1
Probablemente no. No estoy seguro, pero esperaría sizeof(std::function)no ser más que 2 * sizeof(size_t), que es el tamaño mínimo que alguna vez consideraría para una referencia constante.
Mats Petersson el
12
@ Mat: No creo que el tamaño del std::functioncontenedor sea tan importante como la complejidad de copiarlo. Si se trata de copias profundas, podría ser mucho más costoso de lo que sizeofsugiere.
Ben Voigt
¿Deberías movela función en?
Yakk - Adam Nevraumont
operator()()es constasí que una referencia constante debería funcionar. Pero nunca he usado std :: function.
Neel Basu
@Yakk Acabo de pasar un lambda directamente a la función.
Sven Adbring

Respuestas:

79

Si desea rendimiento, pase por valor si lo está almacenando.

Supongamos que tiene una función llamada "ejecutar esto en el hilo de la interfaz de usuario".

std::future<void> run_in_ui_thread( std::function<void()> )

que ejecuta un código en el subproceso "ui", luego señala el futurecuando está hecho. (Útil en los marcos de la interfaz de usuario donde el hilo de la interfaz de usuario es donde se supone que debes jugar con los elementos de la interfaz de usuario)

Tenemos dos firmas que estamos considerando:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A)
std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B)

Ahora, es probable que usemos estos de la siguiente manera:

run_in_ui_thread( [=]{
  // code goes here
} ).wait();

que creará un cierre anónimo (una lambda), construirá un std::functionfuera de él, lo pasará a la run_in_ui_threadfunción y luego esperará a que termine de ejecutarse en el hilo principal.

En el caso (A), std::functionse construye directamente a partir de nuestra lambda, que luego se utiliza dentro de run_in_ui_thread. La lambda se moveintroduce en el std::function, por lo que cualquier estado móvil se transporta de manera eficiente.

En el segundo caso, std::functionse crea un temporal , se moveintroduce el lambda en él, luego ese temporal std::functionse usa como referencia dentro del run_in_ui_thread.

Hasta ahora, todo bien: los dos se desempeñan de manera idéntica. ¡Excepto run_in_ui_threadque va a hacer una copia de su argumento de función para enviar al hilo ui para ejecutar! (regresará antes de terminar con él, por lo que no puede simplemente usar una referencia a él). Para el caso (A), simplemente moveel std::functionen su almacenamiento a largo plazo. En el caso (B), nos vemos obligados a copiar el std::function.

Esa tienda hace que pasar por valor sea más óptimo. Si existe alguna posibilidad de que esté almacenando una copia del std::functionpase por valor. De lo contrario, de cualquier manera es más o menos equivalente: el único inconveniente del std::functionsub -valor es si está tomando el mismo voluminoso y tiene un método secundario tras otro. Salvo eso, a moveserá tan eficiente como a const&.

Ahora, hay algunas otras diferencias entre los dos que se aplican principalmente si tenemos un estado persistente dentro del std::function.

Suponga que std::functionalmacena algún objeto con a operator() const, pero también tiene algunos mutablemiembros de datos que modifica (¡qué grosero!).

En el std::function<> const&caso, los mutablemiembros de datos modificados se propagarán fuera de la llamada a la función. En el std::function<>caso, no lo harán.

Este es un caso de esquina relativamente extraño.

Desea tratar std::functioncomo lo haría con cualquier otro tipo posiblemente pesado y de bajo costo. Mudarse es barato, copiar puede ser costoso.

Yakk - Adam Nevraumont
fuente
La ventaja semántica de "pasar por valor si lo está almacenando", como usted dice, es que por contrato la función no puede mantener la dirección del argumento pasado. ¿Pero es realmente cierto que "salvo eso, un movimiento será tan eficiente como una constante"? Siempre veo el costo de una operación de copia más el costo de la operación de movimiento. Al pasar const&, solo veo el costo de la operación de copia.
ceztko
2
@ceztko En ambos casos (A) y (B), el temporal std::functionse crea a partir de la lambda. En (A), lo temporal se elide en el argumento de run_in_ui_thread. En (B) se pasa una referencia a dicho temporal run_in_ui_thread. Mientras sus std::functions se creen a partir de lambdas como temporales, esa cláusula se mantiene. El párrafo anterior trata del caso donde std::functionpersiste. Si no estamos almacenando, solo creamos desde una lambda function const&y functiontenemos exactamente la misma sobrecarga.
Yakk - Adam Nevraumont
¡Ah, ya veo! Esto, por supuesto, depende de lo que sucede fuera de run_in_ui_thread(). ¿Hay solo una firma que diga "Pasar por referencia, pero no guardaré la dirección"?
ceztko
@ceztko No, no lo hay.
Yakk - Adam Nevraumont
1
@ Yakk-AdamNevraumont si estaría más completo para cubrir otra opción para pasar por el valor de referencia:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P
33

Si le preocupa el rendimiento y no está definiendo una función miembro virtual, lo más probable es que no deba usarlo std::functionen absoluto.

Hacer que el functor escriba un parámetro de plantilla permite una mayor optimización que std::function, incluida la integración de la lógica del functor. Es probable que el efecto de estas optimizaciones supere en gran medida las preocupaciones de copiar frente a indirección sobre cómo pasar std::function.

Más rápido:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}
Ben Voigt
fuente
1
No estoy preocupado en absoluto por el rendimiento en realidad. Solo pensé que usar referencias constantes donde deberían usarse es una práctica común (me vienen a la mente cadenas y vectores).
Sven Adbring
13
@Ben: Creo que la forma más moderna de implementar esto para los hippies es usar std::forward<Functor>(x)();, para preservar la categoría de valor del functor, ya que es una referencia "universal". Sin embargo, no va a hacer una diferencia en el 99% de los casos.
GManNickG
1
@Ben Voigt así que para su caso, ¿llamaría a la función con un movimiento? callFunction(std::move(myFunctor));
arias_JC
2
@arias_JC: si el parámetro es una lambda, ya es un valor r. Si tiene un valor l, puede usarlo std::movesi ya no lo necesitará de otra manera, o pasarlo directamente si no desea salir del objeto existente. Las reglas de colapso de referencia aseguran que callFunction<T&>()tenga un parámetro de tipo T&, no T&&.
Ben Voigt
1
@BoltzmannBrain: Elegí no hacer ese cambio porque solo es válido para el caso más simple, cuando la función se llama solo una vez. Mi respuesta es a la pregunta "¿cómo debo pasar un objeto de función?" y no se limita a una función que no hace nada más que invocar incondicionalmente a ese functor exactamente una vez.
Ben Voigt
25

Como es habitual en C ++ 11, pasar por valor / referencia / referencia constante depende de lo que haga con su argumento. std::functionno es diferente

Pasar por valor le permite mover el argumento a una variable (generalmente una variable miembro de una clase):

struct Foo {
    Foo(Object o) : m_o(std::move(o)) {}

    Object m_o;
};

Cuando sepa que su función moverá su argumento, esta es la mejor solución, de esta manera sus usuarios pueden controlar cómo llaman a su función:

Foo f1{Object()};               // move the temporary, followed by a move in the constructor
Foo f2{some_object};            // copy the object, followed by a move in the constructor
Foo f3{std::move(some_object)}; // move the object, followed by a move in the constructor

Creo que ya conoce la semántica de las (no) referencias constantes, por lo que no voy a pensar en ello. Si necesita que agregue más explicaciones sobre esto, solo pregunte y lo actualizaré.

syam
fuente