2012-05-08 12 views
10

Es bastante conocido que el comportamiento predeterminado de std :: bind y std :: thread es que copiará (o moverá) los argumentos que se le pasen, y para usar la semántica de referencia tendremos que usar wrappers de referencia.¿Cuál es la lógica detrás de std :: bind y std :: thread siempre copiando argumentos?

  1. ¿Alguien sabe por qué esto hace un buen comportamiento predeterminado? Esp. en C++ 11 con referencia rvalue y reenvío perfecto, me parece que tiene más sentido simplemente reenviar perfectamente los argumentos.

  2. std :: make_shared aunque no siempre copia/mueve pero reenvía perfectamente los argumentos proporcionados. ¿Por qué hay dos comportamientos aparentemente diferentes de argumentos de reenvío aquí? (Std :: hilo y std :: bind que siempre copiar/mover vs std :: make_shared que no lo hacen)

+0

Si enlaza una referencia a un local, tendrá problemas. – mfontanini

+1

No lo seré si la función llamada no existe más allá del alcance del local. Hay muchas cosas en C++ que le permiten volar el pie. Mi pregunta es más al punto que este comportamiento parece ser menos intuitivo. – ryaner

+0

En algún momento mi respuesta contenía un "y ese local queda fuera de alcance", pero aparentemente se borró después de la edición: S – mfontanini

Respuesta

13

make_shared se reenvía a un constructor que se llama ahora. Si el constructor usa la semántica llamada por referencia, obtendrá la referencia; si llama por valor, hará una copia. No hay problema aquí de cualquier manera.

bind crea una llamada retrasada a una función que se llama en algunos puntos desconocidos en el futuro, cuando el contexto local se haya ido potencialmente. Si el bind utilizara el reenvío perfecto, tendría que copiar los argumentos que normalmente se envían por referencia y que no se conocen como vigentes en el momento de la llamada real, guárdelos en algún lugar y administre ese almacenamiento. Con la semántica actual bind lo hace por usted.

2

La razón más probable es simplemente que C++ utiliza la semántica de valor por defecto prácticamente en todas partes. Y el uso de referencias podría crear fácilmente problemas relativos a la duración del objeto referido.

+0

¿Pero no es el principio de C++ que el programador sabe lo que quiere? En el caso de referencia oscilante, el programador debe tener cuidado de que los parámetros de su función sean por valor si lo desea, y cuando el parámetro de nuestra función tome una referencia, esperaría que las cosas se reenvíen perfectamente como referencia para cualquier riesgo. Solo un pensamiento. – ryaner

+1

No. El principio es que el programador no paga en rendimiento por funciones que no usa. El hecho de "saber qué estás haciendo" es solo un medio para lograr este fin, no el fin en sí mismo. –

+1

C++ tiene muchos principios :-). Una de las más fundamentales es que usa semántica de valores por defecto; tienes que pedir explícitamente una referencia o un puntero para que haga lo contrario. (Por supuesto, la biblioteca estándar no siempre sigue esta regla: los iteradores y los predicados siempre tienen valor, pero otras cosas tienden a variar). –

1

std :: bind crea un llamable que está separado del sitio de llamada de std::bind, por lo tanto, tiene mucho sentido capturar todos los argumentos por valor de forma predeterminada.

El caso de uso general es ser idéntico a pasar un puntero a una función sin saber dónde podría terminar.

Las lambdas ofrecen más flexibilidad para que el programador decida si la lambda sobrevivirá más allá de donde se capturan los argumentos del alcance.

+0

¿Es el mismo caso para std :: thread? En el caso de la función de inicio de subprocesos, es menos probable que se escriba como lambda. ¿Y forzar el uso del cierre de lambda para pasar por referencia puede parecer extraño? Lambdas no parece resolver todos los problemas causados ​​por esto aquí ... ¿Creo? – ryaner

+0

La primera oración da una buena razón. Votado arriba. Gracias. – ryaner

7

Para ambos std::bind y std::thread, la invocación de la función en los argumentos dados se difiere del sitio de la llamada. En ambos casos, exactamente cuando se llamará a la función es simplemente desconocida.

Reenviar los parámetros directamente en tal caso requeriría almacenar referencias. Lo que puede significar el almacenamiento de referencias a los objetos stack. Que puede no existir cuando la llamada se ejecuta realmente.

Vaya.

Lambdas puede hacerlo porque tiene la capacidad de decidir, por captura, si desea capturar por referencia o por valor. Con std::ref, puede vincular un parámetro por referencia.

+0

Tiene sentido. Votado! Lo aceptaría si se me permitiera aceptar dos respuestas. Gracias. – ryaner

1

De hecho, escribí una pequeña utilidad que crea un funtor de invocación diferida (algo así como std::bind-like, pero sin las funciones anidadas de bind/expressions placeholders).Mi motivación principal era este caso que encontré contrario a la intuición:

using pointer_type = std::unique_ptr<int>; 
pointer_type source(); 
void sink(pointer_type p); 

pointer_type p = source(); 

// Either not valid now or later when calling bound() 
// auto bound = std::bind(sink, std::move(p)); 
auto bound = std::bind(
    [](pointer_type& p) { sink(std::move(p)); } 
    , std::move(p)); 
bound(); 

La razón de que el adaptador (que se mueve a su argumento ref lvalue a sink) es que el retorno de la llamada envoltura por std::bind siempre envía los argumentos ligados como lvalues . Esto no fue un problema con, por ejemplo, boost::bind en C++ 03 dado que lvalue se vincularía a un argumento de referencia del objeto invocable subyacente o a un argumento de valor a través de una copia. No funciona aquí ya que pointer_type es de solo movimiento.

La idea de que lo que tengo es que en realidad hay dos cosas a considerar: cómo los argumentos con destino deben ser almacenados , y cómo deben ser restaurados (es decir, pasa al objeto invocable). El control que le otorga std::bind es el siguiente: los argumentos se almacenan en un fondo (mediante el uso de std::ref) o de manera regular (usando std::decay con avance perfecto); siempre se restauran como valores l (con cv-qualifiers heredados del contenedor de llamadas propietarias). Excepto que puede eludir el último con una pequeña expresión lambda en el sitio como lo acabo de hacer.

Podría decirse que hay mucho control y mucha expresión para aprender relativamente poco. En comparación, mi utilidad tiene semántica como bind(f, p) (copia de decaimiento y almacenamiento, restaurar como lvalue), bind(f, ref(p)) (almacenar superficialmente, restaurar como lvalue), bind(f, std::move(p)) (descomposición y almacenar desde mover, restaurar como valor r), bind(f, emplace(p)) (descomposición y almacenamiento desde movimiento, restaurar como lvalue). Esto se siente como aprender un EDSL.

+0

Es una idea muy interesante, ¿podría indicarnos la implementación para que podamos explorarla? Además, el functor resultante se puede llamar lógicamente una sola vez? ¿Y qué pasó si copiamos el functor resultante? – authchir

+0

@authchir [Code here] (https://bitbucket.org/mickk/annex/src/8166e2d92508/include/annex/make_invoke.hpp) (y cosas como 'emplace' son de [aquí] (https: // bitbucket .org/mickk/annex/src/8166e2d92508/include/annex/val.hpp)). Tiene razón en que es correcto llamar al functor una vez (más llamadas reenviar un 'pointer_type' vacío) y, además de eso, el wrapper de llamadas es de solo movimiento.Esto también se aplica al uso equivalente de 'std :: bind'. –

+0

También [unit test] (https://bitbucket.org/mickk/annex/src/8166e2d92508/unit/make_invoke.cpp) muestra el uso típico. –

Cuestiones relacionadas