pasando lambda como argumento, ¿por referencia o valor?

4 minutos de lectura

He escrito un código de plantilla que toma un funtor como argumento y, después de algún procesamiento, lo ejecuta. Aunque alguien más podría pasar esa función una lambda, un puntero de función o incluso un std::function pero está destinado principalmente a lambda (no es que prohíba otros formatos). Quiero preguntar cómo debo tomar esa lambda, ¿por valor? ¿por referencia? o algo mas.

Código de ejemplo –

#include <iostream>
#include <functional>
using namespace std;

template<typename Functor>
void f(Functor functor)
{
    functor();
}

void g()
{
    cout << "Calling from Function\n";
}

int main() 
{
    int n = 5;

    f([](){cout << "Calling from Temp Lambda\n";});

    f([&](){cout << "Calling from Capturing Temp Lambda\n"; ++n;});

    auto l = [](){cout << "Calling from stored Lambda\n";};
    f(l);

    std::function<void()> funcSTD = []() { cout << "Calling from std::Function\n"; };
    f(funcSTD);

    f(g);
}

En el código anterior, tengo la opción de hacer cualquiera de estos:

template<typename Functor>
    void f(Functor functor)

template<typename Functor>
    void f(Functor &functor)

template<typename Functor>
    void f(Functor &&functor)

¿Cuál sería la mejor manera y por qué? ¿Hay alguna limitación para alguno de estos?

  • En mi humilde opinión, pase por valor.

    – NathanOliver

    21 de febrero de 2017 a las 16:46

  • Siempre he visto objetos con plantillas de funtores pasados ​​por valor, pero no sé por qué; buena pregunta.

    – YSC

    21 de febrero de 2017 a las 16:48

  • relacionado/engañado: stackoverflow.com/questions/8196345/…

    – NathanOliver

    21 de febrero de 2017 a las 16:50

avatar de usuario de skypjack
skypjack

Como posible inconveniente, tenga en cuenta que pasar por copia no podría funcionar si la lambda no se puede copiar. Si puede salirse con la suya, pasar por copia está bien.
Como ejemplo:

#include<memory>
#include<utility>

template<typename F>
void g(F &&f) {
    std::forward<F>(f)();
}

template<typename F>
void h(F f) {
    f();
}

int main() {
    auto lambda = [foo=std::make_unique<int>()](){};

    g(lambda);
    //h(lambda);
}

En el fragmento de arriba, lambda no es copiable debido a foo. Su constructor de copia se elimina como consecuencia del hecho de que el constructor de copia de un std::unique_ptr esta borrado.
Por otro lado, F &&f acepta referencias tanto lvalue como rvalue siendo una referencia de reenvío, así como referencias constantes.
En otros términos, si quieres reutilizar la misma lambda como argumento más de una vez, no puede si sus funciones obtienen su objeto por copia y debe moverlo porque no es copiable (bueno, en realidad puede, es cuestión de envolverlo en un lambda que captura el exterior por referencia).

  • @NathanOliver Por supuesto, pero ya no puedes reutilizar la lambda. Si quieres pasarlo a dos funciones como en el ejemplo, no es posible.

    – skypjack

    21 de febrero de 2017 a las 17:07

  • Pregunta de seguimiento de @skypjack: ¿es posible recurrir al funtor móvil en un escenario como este que no sea definir dos sobrecargas similares?

    – Abhinav Gauniyal

    21 de febrero de 2017 a las 17:15

  • @AbhinavGauniyal Al definir dos sobrecargas que hacen exactamente el mismo trabajo, ¿para poder solo no usar una referencia de reenvío? No obtengo exactamente los beneficios. ¿Entendí mal algo?

    – skypjack

    21 de febrero de 2017 a las 17:18

  • @skypjack Quise decir tener dos definiciones de h() donde primero se toma por valor y 2do por &&.. Para que la compilación no falle.

    – Abhinav Gauniyal

    21 de febrero de 2017 a las 17:19


  • @Nik-Lz no es la mejor definición de la historia, pero se acerca a la realidad. 😉

    – skypjack

    3 de octubre de 2018 a las 7:01

Como las expresiones lambda pueden tener sus propios campos (como clases), copiar/usar referencias puede generar resultados diferentes. Aquí hay un ejemplo simple:

template<class func_t>
size_t call_copy(func_t func) {
    return func(1);
}

template<class func_t>
size_t call_ref(func_t& func) {
    return func(1);
}

int main() {
    auto lambda = [a = size_t{0u}] (int val) mutable {
        return (a += val);
    };
    lambda(5);

    // call_ref(lambda); – uncomment to change result from 5 to 6
    call_copy(lambda);

    std::cout << lambda(0) << std::endl;

    return 0;
}

Por cierto, si quieres juzgarlo por el rendimiento, en realidad no hay diferencia, las lambdas son muy pequeñas y fáciles de copiar.

Además, si desea pasar lambda como parámetro (no una variable que lo contiene), debe usar la referencia de reenvío para que funcione.

  • “Como las expresiones lambda pueden tener sus propios campos” y “las lambdas son muy pequeñas y fáciles de copiar” son dos afirmaciones contradictorias. El tamaño de una lambda depende del tamaño de estos campos. Se puede crear una lambda arbitrariamente grande y muy difícil de copiar.

    – AnT apoya a Rusia

    9 de febrero de 2019 a las 15:27

¿Ha sido útil esta solución?