¿Por qué lambda de C ++ 11 requiere una palabra clave "mutable" para captura por valor, por defecto?

256

Breve ejemplo:

#include <iostream>

int main()
{
    int n;
    [&](){n = 10;}();             // OK
    [=]() mutable {n = 20;}();    // OK
    // [=](){n = 10;}();          // Error: a by-value capture cannot be modified in a non-mutable lambda
    std::cout << n << "\n";       // "10"
}

La pregunta: ¿Por qué necesitamos la mutablepalabra clave? Es bastante diferente del paso de parámetros tradicionales a funciones con nombre. ¿Cuál es la razón detrás?

Tenía la impresión de que el objetivo de la captura por valor es permitir que el usuario cambie el temporal; de lo contrario, casi siempre estoy mejor usando la captura por referencia, ¿no?

¿Alguna iluminación?

(Por cierto, estoy usando MSVC2010. AFAIK esto debería ser estándar)

kizzx2
fuente
101
Buena pregunta; ¡aunque me alegro de que finalmente haya algo constpor defecto!
xtofl
3
No es una respuesta, pero creo que esto es algo sensato: si tomas algo por valor, no deberías cambiarlo solo para guardar 1 copia en una variable local. Al menos no cometerá el error de cambiar n reemplazando = con &.
stefaanv
8
@xtofl: No estoy seguro de que sea bueno, cuando todo lo demás no es constpor defecto.
kizzx2
8
@ Tamás Szelei: No para comenzar una discusión, pero en mi humilde opinión, el concepto "fácil de aprender" no tiene cabida en el lenguaje C ++, especialmente en los días modernos. De todos modos: P
kizzx2
3
"El punto completo de la captura por valor es permitir que el usuario cambie el temporal" - No, el punto completo es que el lambda puede permanecer válido más allá de la vida útil de cualquier variable capturada. Si las lambdas de C ++ solo tuvieran captura por referencia, serían inutilizables en demasiados escenarios.
Sebastian Redl

Respuestas:

230

Requiere mutableporque, de manera predeterminada, un objeto de función debería producir el mismo resultado cada vez que se llama. Esta es la diferencia entre una función orientada a objetos y una función que utiliza una variable global, efectivamente.

Perrito
fuente
77
Este es un buen punto. Estoy totalmente de acuerdo. Sin embargo, en C ++ 0x, no entiendo cómo el valor predeterminado ayuda a aplicar lo anterior. Considere que estoy en el extremo receptor de la lambda, por ejemplo, lo estoy void f(const std::function<int(int)> g). ¿Cómo puedo garantizar que gsea ​​realmente referencialmente transparente ? gEl proveedor de 'podría haber utilizado de mutabletodos modos. Entonces no lo sabré. Por otro lado, si el valor predeterminado es non- const, y las personas deben agregar en constlugar de mutableobjetos de función, el compilador puede hacer cumplir la const std::function<int(int)>parte y ahora fpuede asumir que ges const, ¿no?
kizzx2 01 de
8
@ kizzx2: en C ++, no se aplica nada , solo se sugiere. Como de costumbre, si haces algo estúpido (requisito documentado para la transparencia referencial y luego pasas una función transparente no referencial), obtienes lo que sea que se te ocurra.
Puppy
66
Esta respuesta me abrió los ojos. Anteriormente, pensé que en este caso lambda solo muta una copia para la "ejecución" actual.
Zsolt Szatmari
44
@ZsoltSzatmari ¡Tu comentario me abrió los ojos! : -DI no obtuvo el verdadero significado de esta respuesta hasta que leí tu comentario.
Jendas
55
No estoy de acuerdo con la premisa básica de esta respuesta. C ++ no tiene el concepto de "las funciones siempre deben devolver el mismo valor" en cualquier otro lugar del lenguaje. Como principio de diseño, estaría de acuerdo en que es una buena manera de escribir una función, pero no creo que contenga el agua como la razón del comportamiento estándar.
Ionoclast Brigham
103

Su código es casi equivalente a esto:

#include <iostream>

class unnamed1
{
    int& n;
public:
    unnamed1(int& N) : n(N) {}

    /* OK. Your this is const but you don't modify the "n" reference,
    but the value pointed by it. You wouldn't be able to modify a reference
    anyway even if your operator() was mutable. When you assign a reference
    it will always point to the same var.
    */
    void operator()() const {n = 10;}
};

class unnamed2
{
    int n;
public:
    unnamed2(int N) : n(N) {}

    /* OK. Your this pointer is not const (since your operator() is "mutable" instead of const).
    So you can modify the "n" member. */
    void operator()() {n = 20;}
};

class unnamed3
{
    int n;
public:
    unnamed3(int N) : n(N) {}

    /* BAD. Your this is const so you can't modify the "n" member. */
    void operator()() const {n = 10;}
};

int main()
{
    int n;
    unnamed1 u1(n); u1();    // OK
    unnamed2 u2(n); u2();    // OK
    //unnamed3 u3(n); u3();  // Error
    std::cout << n << "\n";  // "10"
}

Por lo tanto, podría pensar que lambdas genera una clase con operator () que por defecto es constante, a menos que diga que es mutable.

También puede pensar en todas las variables capturadas dentro de [] (explícita o implícitamente) como miembros de esa clase: copias de los objetos para [=] o referencias a los objetos para [&]. Se inicializan cuando declara su lambda como si hubiera un constructor oculto.

Daniel Muñoz
fuente
55
Si bien una buena explicación de cómo se vería un consto mutablelambda si se implementara como tipos equivalentes definidos por el usuario, la pregunta es (como en el título y elaborado por OP en los comentarios) por qué const es el valor predeterminado, por lo que esto no lo responde.
underscore_d
36

Tenía la impresión de que el objetivo de la captura por valor es permitir que el usuario cambie el temporal; de lo contrario, casi siempre estoy mejor usando la captura por referencia, ¿no?

La pregunta es, ¿es "casi"? Un caso de uso frecuente parece ser devolver o pasar lambdas:

void registerCallback(std::function<void()> f) { /* ... */ }

void doSomething() {
  std::string name = receiveName();
  registerCallback([name]{ /* do something with name */ });
}

Creo que mutableno es un caso de "casi". Considero que "captura por valor" como "permitirme usar su valor después de que la entidad capturada muera" en lugar de "permitirme cambiar una copia de ella". Pero tal vez esto pueda ser discutido.

Johannes Schaub - litb
fuente
2
Buen ejemplo. Este es un caso de uso muy fuerte para el uso de captura por valor. Pero, ¿por qué es predeterminado const? ¿Qué propósito logra? mutableparece fuera de lugar aquí, cuando noconst es el valor predeterminado en "casi" (: P) todo lo demás del lenguaje.
kizzx2
8
@ kizzx2: Ojalá constfuera el valor predeterminado, al menos las personas se verían obligadas a considerar la corrección const: /
Matthieu M.
1
@ kizzx2 mirando los documentos lambda, me parece que lo hacen por defecto para constque puedan llamarlo si el objeto lambda es constante o no. Por ejemplo, podrían pasarlo a una función tomando a std::function<void()> const&. Para permitir que el lambda cambie sus copias capturadas, en los documentos iniciales los miembros de datos del cierre se definieron mutableinternamente de forma automática. Ahora tiene que poner manualmente mutablela expresión lambda. Sin embargo, no he encontrado una justificación detallada.
Johannes Schaub - litb
2
Ver open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2651.pdf para algunos detalles.
Johannes Schaub - litb
55
En este punto, para mí, la respuesta / justificación "real" parece ser "no pudieron evitar un detalle de implementación": /
kizzx2
32

FWIW, Herb Sutter, un miembro bien conocido del comité de estandarización de C ++, ofrece una respuesta diferente a esa pregunta en Corriente de Lambda y Problemas de usabilidad :

Considere este ejemplo, donde el programador captura una variable local por valor e intenta modificar el valor capturado (que es una variable miembro del objeto lambda):

int val = 0;
auto x = [=](item e)            // look ma, [=] means explicit copy
            { use(e,++val); };  // error: count is const, need ‘mutable’
auto y = [val](item e)          // darnit, I really can’t get more explicit
            { use(e,++val); };  // same error: count is const, need ‘mutable’

Esta característica parece haberse agregado debido a la preocupación de que el usuario podría no darse cuenta de que recibió una copia, y en particular de que dado que las lambdas son copiables, podría estar cambiando una copia de lambda diferente.

Su artículo trata sobre por qué esto debería cambiarse en C ++ 14. Es breve, está bien escrito, vale la pena leerlo si quiere saber "qué hay en la mente [de los miembros del comité]" con respecto a esta característica en particular.

akim
fuente
16

Debe pensar cuál es el tipo de cierre de su función Lambda. Cada vez que declara una expresión Lambda, el compilador crea un tipo de cierre, que es nada menos que una declaración de clase sin nombre con atributos ( entorno donde se declaró la expresión Lambda) y la llamada a la función ::operator()implementada. Cuando captura una variable utilizando una copia por valor , el compilador creará un nuevo constatributo en el tipo de cierre, por lo que no puede cambiarlo dentro de la expresión Lambda porque es un atributo de "solo lectura", esa es la razón por la que llámelo " cierre ", porque de alguna manera, está cerrando su expresión Lambda copiando las variables del alcance superior en el alcance Lambda.mutable, la entidad capturada se convertirá en un non-constatributo de su tipo de cierre. Esto es lo que hace que los cambios realizados en la variable mutable capturados por el valor, no se propaguen al alcance superior, sino que se mantengan dentro del Lambda con estado. Siempre trate de imaginar el tipo de cierre resultante de su expresión Lambda, que me ayudó mucho, y espero que también pueda ayudarlo.

Tarántula
fuente
14

Ver este borrador , en 5.1.2 [expr.prim.lambda], subcláusula 5:

El tipo de cierre para una expresión lambda tiene un operador público de llamada a función en línea (13.5.4) cuyos parámetros y tipo de retorno se describen mediante la cláusula-declaración-parámetro y el tipo de retorno de trailing de la expresión lambda, respectivamente. Este operador de llamada de función se declara const (9.3.1) si y solo si la cláusula-declaración-parámetro de lambdaexpression no es seguida por mutable.

Edite el comentario de litb: ¿Tal vez pensaron en la captura por valor para que los cambios externos a las variables no se reflejen dentro de la lambda? Las referencias funcionan en ambos sentidos, así que esa es mi explicación. Sin embargo, no sé si es bueno.

Edite en el comentario de kizzx2: la mayoría de las veces cuando se va a usar un lambda es como un functor para algoritmos. El valor predeterminado constpermite que se use en un entorno constante, al igual que las constfunciones calificadas normales se pueden usar allí, pero las no constcalificadas no. Tal vez solo pensaron en hacerlo más intuitivo para esos casos, que saben lo que sucede en su mente. :)

Xeo
fuente
Es el estándar, pero ¿por qué lo escribieron de esta manera?
kizzx2
@ kizzx2: Mi explicación está directamente debajo de esa cita. :) Se relaciona un poco con lo que dice Litb sobre la vida útil de los objetos capturados, pero también va un poco más allá.
Xeo
@Xeo: Ah, sí, me perdí eso: P También es otra buena explicación para un buen uso de la captura por valor . Pero, ¿por qué debería ser constpor defecto? Ya obtuve una copia nueva, parece extraño que no me dejen cambiarla, especialmente porque no es algo principalmente malo, solo quieren que agregue mutable.
kizzx2
Creo que hubo un intento de crear una nueva sintaxis de declaración de función genral, parecida a una lambda con nombre. También se suponía que solucionaría otros problemas al hacer que todo const por defecto. Nunca se completó, pero las ideas se contagiaron con la definición lambda.
Bo Persson
2
@ kizzx2: si pudiéramos comenzar de nuevo, probablemente tendríamos varuna palabra clave para permitir el cambio y ser el valor predeterminado constante para todo lo demás. Ahora no, así que tenemos que vivir con eso. En mi opinión, C ++ 2011 salió bastante bien, teniendo en cuenta todo.
Bo Persson
11

Tenía la impresión de que el objetivo de la captura por valor es permitir que el usuario cambie el temporal; de lo contrario, casi siempre estoy mejor usando la captura por referencia, ¿no?

nNo es temporal. n es un miembro del objeto de función lambda que crea con la expresión lambda. La expectativa predeterminada es que llamar a su lambda no modifica su estado, por lo tanto, es constante para evitar que lo modifique accidentalmente n.

Martin Ba
fuente
1
Todo el objeto lambda es temporal, sus miembros también tienen una vida útil temporal.
Ben Voigt
2
@Ben: IIRC, me refería al problema de que cuando alguien dice "temporal", entiendo que significa un objeto temporal sin nombre , que es la lambda en sí, pero sus miembros no lo son. Y también que desde "dentro" de la lambda, realmente no importa si la lambda es temporal. Al releer la pregunta, parece que OP solo quería decir "n dentro de la lambda" cuando dijo "temporal".
Martin Ba
6

¡Tienes que entender lo que significa capturar! ¡está capturando no pasando argumentos! Veamos algunos ejemplos de código:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() {return x + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //output 10,20

}

Como puede ver a pesar de que xse ha cambiado a 20lambda, todavía está devolviendo 10 ( xtodavía está 5dentro de la lambda) Cambiar xdentro de la lambda significa cambiar la lambda en cada llamada (la lambda está mutando en cada llamada). Para hacer cumplir la corrección, el estándar introdujo la mutablepalabra clave. Al especificar una lambda como mutable, está diciendo que cada llamada a la lambda podría causar un cambio en la lambda misma. Veamos otro ejemplo:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() mutable {return x++ + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //outputs 11,20

}

El ejemplo anterior muestra que al hacer que la lambda sea mutable, cambiar xdentro de la lambda "muta" la lambda en cada llamada con un nuevo valor xque no tiene nada que ver con el valor real de xen la función principal

Soulimane Mammar
fuente
4

Ahora hay una propuesta para aliviar la necesidad de las mutabledeclaraciones lambda: n3424

usta
fuente
¿Alguna información sobre lo que vino de eso? Personalmente, creo que es una mala idea, ya que la nueva "captura de expresiones arbitrarias" suaviza la mayoría de los puntos débiles.
Ben Voigt
1
@BenVoigt Sí, parece un cambio por el bien del cambio.
Miles Rout
3
@BenVoigt Aunque para ser justos, espero que probablemente haya muchos desarrolladores de C ++ que no saben que mutableincluso es una palabra clave en C ++.
Miles Rout
1

Para extender la respuesta de Puppy, las funciones lambda están destinadas a ser funciones puras . Eso significa que cada llamada dada un conjunto de entrada único siempre devuelve la misma salida. Definamos la entrada como el conjunto de todos los argumentos más todas las variables capturadas cuando se llama a lambda.

En las funciones puras, la salida depende únicamente de la entrada y no de algún estado interno. Por lo tanto, cualquier función lambda, si es pura, no necesita cambiar su estado y, por lo tanto, es inmutable.

Cuando una lambda captura por referencia, escribir en variables capturadas es una carga para el concepto de función pura, porque todo lo que una función pura debería hacer es devolver una salida, aunque la lambda ciertamente no muta porque la escritura sucede a variables externas. Incluso en este caso, un uso correcto implica que si se llama a la lambda con la misma entrada nuevamente, la salida será la misma cada vez, a pesar de estos efectos secundarios en las variables by-ref. Tales efectos secundarios son solo formas de devolver alguna entrada adicional (por ejemplo, actualizar un contador) y podrían reformularse en una función pura, por ejemplo, devolver una tupla en lugar de un solo valor.

Attersson
fuente