Pregunta std :: thread taking lambda con ref arg no compila


Estoy leyendo C ++ concurrencia en acción. El Capítulo 2.4 describe un algoritmo parallelll_accumulate.

Intenté, como experimento de aprendizaje, reemplazar el functor utilizado allí, con un lambda genérico.

He destilado el error de compilación a:

#include <thread>

template <typename T>
struct f {
    void operator() (T& result) { result = 1;}
};

int main() {
    int x = 0;
    auto g = [](auto& result) { result = 1; };

    std::thread(f<int>(), std::ref(x));  // COMPILES
    std::thread(g, std::ref(x));         // FAILS TO COMPILE
}

El mensaje de error:

 In file included from /usr/include/c++/4.9/thread:39:0,
                 from foo.cpp:1:
/usr/include/c++/4.9/functional: In instantiation of ‘struct std::_Bind_simple<main()::<lambda(auto:1&)>(std::reference_wrapper<int>)>’:
/usr/include/c++/4.9/thread:140:47:   required from ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = main()::<lambda(auto:1&)>&; _Args = {std::reference_wrapper<int>}]’
foo.cpp:13:31:   required from here
/usr/include/c++/4.9/functional:1665:61: error: no type named ‘type’ in ‘class std::result_of<main()::<lambda(auto:1&)>(std::reference_wrapper<int>)>’
       typedef typename result_of<_Callable(_Args...)>::type result_type;
                                                             ^
/usr/include/c++/4.9/functional:1695:9: error: no type named ‘type’ in ‘class std::result_of<main()::<lambda(auto:1&)>(std::reference_wrapper<int>)>’
         _M_invoke(_Index_tuple<_Indices...>)
         ^

Mi versión compiladora

$ g++ --version
g++ (Ubuntu 4.9.1-16ubuntu6) 4.9.1

¿Por qué la compilación falla para el lambda pero no para el functor?

EDITAR: ¿Cómo puedo lograr lo que el functor está haciendo (asignar a un árbitro) con un lambda genérico?


10
2018-01-26 16:32


origen


Respuestas:


Otra variación en el mismo tema que la deducción del argumento de la plantilla no se ve a través de las conversiones.

los operator() de f<int> es

void operator() (int& result);

cuando pasas un reference_wrapper<int> para ello, la función de conversión (operator int &) se llama, produciendo una referencia que puede estar vinculada a result.

los operator() de su lambda genérico es

template<class T> void operator() (T& result) const;

Si se pasó a reference_wrapper lvalue, deduciría T como un reference_wrapper y luego no compila en la tarea. (Asignación a un reference_wrapper vuelve a colocar la "referencia" en lugar de afectar el valor).

Pero falla incluso antes de eso, porque el estándar requiere que lo que pase a std::thread debe poderse llamar con prvalues, y una referencia de valor l no const no se vincula a un prvalue. Este es el error que ves - result_of no contiene type porque su functor no se puede llamar para el tipo de argumento. Si intentas hacer g(std::ref(x));, clang produce un error bastante claro:

main.cpp:16:5: error: no matching function for call to object of type '(lambda at main.cpp:11:14)'
    g(std::ref(x));
    ^
main.cpp:11:14: note: candidate function [with $auto-0-0 = std::__1::reference_wrapper<int>] not viable: expects an l-value for 1st argument
    auto g = [](auto& result) { result = 1; };         
    ^

Probablemente deberías considerar capturar el local relevante por referencia:

auto g = [&x]() { x = 1; };

O si, por alguna razón, debe usar un lambda genérico, entonces puede tomar un reference_wrapper por valor (o por referencia constante), y luego desenvolver usando get():

 auto g = [](auto result) { result.get() = 1; };

o tal vez agregar un std::bind que desenvolverá el reference_wrappers, lo que permite que la deducción del argumento de la plantilla haga lo correcto (hat tip @Casey):

 std::thread(std::bind(g, std::ref(x)));

o tal vez prescindir de esto reference_wrapper sin sentido y escriba su lambda para tomar un puntero no propietario en su lugar:

auto g = [](auto* result) { *result = 1; };
std::thread(g, &x);

17
2018-01-26 16:48



Hay todo tipo de problemas relacionados con la transmisión de argumentos a través de la familia de funciones "INVOKE (...)" std::async, std::bind, std::thread::thread. Si desea utilizar un nombre de función sobrecargado, o pasar una referencia lvalue, o que Dios prohíba pasar un valor r por referencia, va a tener un momento difícil. Vendrás aquí para SO y uno de nosotros que haya aprendido el conjuro relevante te lo transmitirá. Con suerte lo recordarás la próxima vez que aparezca.

Creo que la mejor práctica desde C ++ 14 es evitar el argumento pasando rarezas por completo manejando los argumentos usted mismo y siempre dando a las funciones INVOKE un functor de argumento cero que encapsula los argumentos requeridos por la función objetivo real. Hacerlo usted mismo le permite obtener exactamente la semántica que pretende sin tener que conocer cada capricho y solución y las finas distinciones en las interfaces de las funciones de la familia INVOKE. La captura lambda generalizada de C ++ 14 hace que sea bastante simple encapsular cualquier tipo de función y conjunto de argumentos.

En su caso, este enfoque daría como resultado:

#include <thread>

template <typename T>
struct f {
    void operator() (T& result) { result = 1;}
};

int main() {
    int x = 0;
    auto g = [](auto& result) { result = 1; };

    std::thread([&]{ return f<int>{}(x); });
    std::thread([&]{ return g(x); });
}

que se desempeña exactamente como se esperaba y es más legible.

std::reference_wrapper fue genial en los días TR1 cuando necesitábamos pasar referencias a través de std::bind, pero sus días de gloria han pasado y creo que es mejor evitarlos en C ++ moderno.


5
2018-01-26 17:52



Aquí hay una función para resolver su problema. Toma un objeto de función y devuelve un objeto de función que desempaquetará std::reference_wrappers antes de pasarlo al objeto de función interno.

#include <utility>
#include <functional>

template<class T>
T&& unref( T&& t ){return std::forward<T>(t);}
template<class T>
T& unref( std::reference_wrapper<T> r ){ return r.get(); }

template<class F>
auto launder_refs( F&& f ) {
  return [f = std::forward<F>(f)](auto&&... args){
    return f( unref( std::forward<decltype(args)>(args) )... );
  };
}

//

  auto g = launder_refs([](auto& result) { result = 2; });

ejemplo en vivo -- ahora g se comporta como tu original g, excepto cuando pasó std::reference_wrappers los convierte en referencias antes de pasarlos a la result dentro.

Tu problema es que std::reference_wrapper<T>&& pasó a su lambda hace que trate de deducir una U tal que U& = std::reference_wrapper<T>&&y ninguno existe

En resumen, esta es una limitación de tipo de deducción en las funciones de la plantilla (no considera las conversiones), ya que mezclar las conversiones y la deducción del tipo de plantilla en el mismo paso conduciría a todos a la rudeza.

El código anterior oculta el std::reference_wrappers del cierre lambda subyacente (u objeto de función). Lo hace con un mínimo de gastos generales también.


1
2018-01-26 20:55