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 ++?
c++
c++11
lambda
language-lawyer
Colita
fuente
fuente
Respuestas:
Un lambda no puede capturar una referencia en sí por valor (uso
std::reference_wrapper
para tal fin).En su lambda, las
[m]
capturasm
por valor (porque no hay ninguna&
en la captura), por lo quem
(siendo una referencia an
) primero se desreferencia y se captura una copia de la cosa a la que hace referencia (n
). Esto no es diferente a hacer esto: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 capturam
por referencia en su lugar:[&m]
. Esto no es diferente a asignar una referencia a otra, por ejemplo:O bien, puede simplemente deshacerse de
m
todo y capturan
por referencia en su lugar:[&n]
.Aunque, dado que
n
tiene un alcance global, realmente no necesita ser capturado, la lambda puede acceder a él globalmente sin capturarlo:fuente
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
m
se 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
n
no 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
m
en 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
m
para que ya no sea una expresión constante:En este caso, todos los compiladores están de acuerdo en que la salida es
porque
m
en lambda se referirá al miembro del cierre que es de tipoint
copy-initialized de la variable de referenciam
enf
.fuente
m
es 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.m += 123;
Aquím
está odr-used.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 :
Los
[m]
medios de que la variablem
enf
es capturado por la copia. La entidadm
es 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 esint
y noint&
.Dado que el nombre
m
dentro del cuerpo lambda nombra el miembro del objeto de cierre y no la variable enf
(y esta es la parte cuestionable), la instrucciónm += 123;
modifica ese miembro, que es unint
objeto diferente de::n
.fuente