Diferencia de comportamiento de la captura mutable de la función lambda a partir de una referencia a la variable global

22

Descubrí que los resultados son diferentes entre los compiladores si uso un lambda para capturar una referencia a una variable global con una palabra clave mutable y luego modifico el valor en la función lambda.

#include <stdio.h>
#include <functional>

int n = 100;

std::function<int()> f()
{
    int &m = n;
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

Resultado de VS 2015 y GCC (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

Resultado de clang ++ (clang versión 3.8.0-2ubuntu4 (etiquetas / RELEASE_380 / final)):

100 223 223

¿Por qué pasó esto? ¿Está permitido por los estándares de C ++?

Colita
fuente
El comportamiento de Clang todavía está presente en el tronco.
nogal
Estas son todas versiones de compilador bastante antiguas
MM
Todavía se presenta en la versión reciente de Clang: godbolt.org/z/P9na9c
Willy
1
Si elimina la captura por completo, entonces GCC aún acepta este código y hace lo que hace el sonido metálico. Esa es una fuerte pista de que hay un error de CCG: se supone que las capturas simples no cambian el significado del cuerpo lambda.
TC

Respuestas:

16

Un lambda no puede capturar una referencia en sí por valor (uso std::reference_wrapperpara tal fin).

En su lambda, las [m]capturas mpor valor (porque no hay ninguna &en la captura), por lo que m(siendo una referencia a n) primero se desreferencia y se captura una copia de la cosa a la que hace referencia ( n). Esto no es diferente a hacer esto:

int &m = n;
int x = m; // <-- copy made!

La lambda luego modifica esa copia, no el original. Eso es lo que está viendo suceder en las salidas VS y GCC, como se esperaba.

La salida de Clang es incorrecta, y debe informarse como un error, si aún no lo ha hecho.

Si desea que su lambda de modificar n, la captura mpor referencia en su lugar: [&m]. Esto no es diferente a asignar una referencia a otra, por ejemplo:

int &m = n;
int &x = m; // <-- no copy made!

O bien, puede simplemente deshacerse de mtodo y captura npor referencia en su lugar: [&n].

Aunque, dado que ntiene un alcance global, realmente no necesita ser capturado, la lambda puede acceder a él globalmente sin capturarlo:

return [] () -> int {
    n += 123;
    return n;
};
Remy Lebeau
fuente
5

Creo que Clang en realidad puede ser correcto.

De acuerdo con [lambda.capture] / 11 , una expresión id utilizada en el lambda se refiere al miembro de lambda capturado por copia solo si constituye un uso odr . Si no es así, se refiere a la entidad original . Esto se aplica a todas las versiones de C ++ desde C ++ 11.

De acuerdo con [++.dev.odr] / 3 de C ++ 17, una variable de referencia no se utiliza odr si la conversión de valor-valor-valor produce una expresión constante.

Sin embargo, en el borrador de C ++ 20, el requisito para la conversión de valor a valor se cae y el pasaje relevante cambió varias veces para incluir o no la conversión. Consulte el número 1472 de CWG y el número 1741 de CWG , así como el número 2083 de CWG abierto .

Como mse inicializa con una expresión constante (que se refiere a un objeto de duración de almacenamiento estático), su uso produce una expresión constante por excepción en [expr.const] /2.11.1 .

Sin embargo, este no es el caso si se aplican las conversiones de valor a valor, porque el valor de nno es utilizable en una expresión constante.

Por lo tanto, dependiendo de si se supone que las conversiones lvalue-to-rvalue se deben aplicar para determinar el uso de odr, cuando se usa men el lambda, puede referirse o no al miembro del lambda.

Si se debe aplicar la conversión, GCC y MSVC son correctos, de lo contrario, Clang lo es.

Puede ver que Clang cambia su comportamiento si cambia la inicialización de mpara que ya no sea una expresión constante:

#include <stdio.h>
#include <functional>

int n = 100;

void g() {}

std::function<int()> f()
{
    int &m = (g(), n);
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

En este caso, todos los compiladores están de acuerdo en que la salida es

100 223 100

porque men lambda se referirá al miembro del cierre que es de tipo intcopy-initialized de la variable de referencia men f.

nuez
fuente
¿Son correctos los resultados de VS / GCC y Clang? ¿O solo uno de ellos?
Willy
[basic.dev.odr] / 3 dice que la variable mes odr-utilizada por una expresión nombrándola a menos que la aplicación de la conversión lvalue-to-rvalue sea una expresión constante. Por [expr.const] / (2.7), esa conversión no sería una expresión constante central.
Aschepler
Si el resultado de Clang es correcto, creo que de alguna manera es contradictorio. Porque desde el punto de vista del programador, debe asegurarse de que la variable que escribe en la lista de captura se esté copiando realmente para el caso mutable, y la inicialización de m podría ser cambiada por el programador más tarde por alguna razón.
Willy
1
m += 123;Aquí mestá odr-used.
Oliv
1
Creo que Clang está en lo cierto con la redacción actual, y aunque no he profundizado en esto, los cambios relevantes aquí son casi todos DR.
TC
4

Esto no está permitido por el estándar C ++ 17, pero por otros borradores estándar podría estarlo. Es complicado, por razones no explicadas en esta respuesta.

[expr.prim.lambda.capture] / 10 :

Para cada entidad capturada por copia, se declara un miembro de datos no estático sin nombre en el tipo de cierre. El orden de declaración de estos miembros no está especificado. El tipo de dicho miembro de datos es el tipo referenciado si la entidad es una referencia a un objeto, una referencia de valor al tipo de función referenciada si la entidad es una referencia a una función, o el tipo de la entidad capturada correspondiente de lo contrario.

Los [m]medios de que la variable men fes capturado por la copia. La entidad mes una referencia al objeto, por lo que el tipo de cierre tiene un miembro cuyo tipo es el tipo referenciado. Es decir, el tipo de miembro es inty no int&.

Dado que el nombre mdentro del cuerpo lambda nombra el miembro del objeto de cierre y no la variable en f(y esta es la parte cuestionable), la instrucción m += 123;modifica ese miembro, que es un intobjeto diferente de ::n.

aschepler
fuente