Diferença de comportamento da captura mutável da função lambda de uma referência à variável global

22

Descobri que os resultados são diferentes nos compiladores se eu usar um lambda para capturar uma referência à variável global com palavra-chave mutável e modificar o valor na função 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 do VS 2015 e GCC (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

Resultado do clang ++ (versão 3.8.0-2ubuntu4 (tags / RELEASE_380 / final)):

100 223 223

Por que isso acontece? Isso é permitido pelos padrões C ++?

Willy
fonte
O comportamento de Clang ainda está presente no porta-malas.
noz
Estas são versões bastante antigas do compilador
MM
Ainda apresenta na versão recente do Clang: godbolt.org/z/P9na9c
Willy
11
Se você remover completamente a captura, o GCC ainda aceitará esse código e fará o que o clang faz. Essa é uma forte dica de que existe um bug do GCC - as capturas simples não devem alterar o significado do corpo lambda.
TC

Respostas:

16

A lambda não pode capturar uma referência em si por valor (uso std::reference_wrapperpara esse fim).

Em seu lambda, as [m]capturas mpor valor (porque não há &na captura), portanto m(sendo uma referência a n) é primeiro desreferenciado e uma cópia do que está fazendo referência ( n) é capturada. Isso não é diferente de fazer isso:

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

O lambda modifica essa cópia, não o original. É o que você está vendo acontecer nas saídas VS e GCC, conforme o esperado.

A saída do Clang está errada e deve ser relatada como um bug, se ainda não o tiver.

Se você quiser que o seu lambda para modificar n, a captura mpor referência em vez disso: [&m]. Isso não é diferente de atribuir uma referência a outra, por exemplo:

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

Ou, você pode simplesmente se livrar mcompletamente e captura npor referência em vez disso: [&n].

Embora, como nesteja no escopo global, realmente não precise ser capturado, o lambda pode acessá-lo globalmente sem capturá-lo:

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

Eu acho que Clang pode estar correto.

De acordo com [lambda.capture] / 11 , uma expressão id usada no lambda refere-se ao membro capturado por cópia do lambda somente se ele constitui um uso de odr . Caso contrário, refere-se à entidade original . Isso se aplica a todas as versões do C ++ desde o C ++ 11.

De acordo com [basic.dev.odr] / 3 do C ++ 17/3, uma variável de referência não é usada odr se a aplicação de conversão lvalue em rvalue produz uma expressão constante.

No rascunho do C ++ 20, no entanto, o requisito para a conversão lvalue em rvalue é descartado e a passagem relevante é alterada várias vezes para incluir ou não a conversão. Consulte a edição 1472 do CWG e a edição 1741 do CWG , bem como a edição 2083 do CWG aberta .

Como mé inicializado com uma expressão constante (referindo-se a um objeto de duração de armazenamento estático), usá-lo gera uma expressão constante por exceção em [expr.const] /2.11.1 .

Esse não é o caso, no entanto, se as conversões lvalue para rvalue forem aplicadas, porque o valor de nnão é utilizável em uma expressão constante.

Portanto, dependendo se as conversões lvalue para rvalue devem ou não ser aplicadas na determinação do uso de odr, quando você usa mo lambda, ele pode ou não se referir ao membro do lambda.

Se a conversão for aplicada, o GCC e o MSVC estão corretos, caso contrário, Clang está.

Você pode ver que o Clang altera seu comportamento se você alterar a inicialização de mpara não ser mais uma expressão 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;
}

Nesse caso, todos os compiladores concordam que a saída é

100 223 100

porque mno lambda irá referir-se ao membro do fecho, que é do tipo de intcópia-inicializada a partir da variável de referência mem f.

noz
fonte
Os resultados de VS / GCC e Clang estão corretos? Ou apenas um deles?
Willy
[basic.dev.odr] / 3 diz que a variável mé odr usada por uma expressão que a nomeia, a menos que aplicar a conversão lvalue em rvalue seja uma expressão constante. Por [expr.const] / (2.7), essa conversão não seria uma expressão constante do núcleo.
aschepler 27/03
Se o resultado de Clang estiver correto, acho que de alguma forma é contra-intuitivo. Porque, do ponto de vista do programador, ele precisa garantir que a variável que ele escreve na lista de capturas seja realmente copiada para casos mutáveis, e a inicialização de m possa ser alterada pelo programador posteriormente por algum motivo.
Willy
11
m += 123;Aqui mé usado por odr.
Oliv
11
Acho que Clang está certo com a redação atual e, embora não tenha me aprofundado nisso, as mudanças relevantes aqui são quase certamente todas as DRs.
TC
4

Isso não é permitido pelo C ++ 17 Standard, mas pode ser por alguns outros rascunhos do Standard. É complicado, por razões não explicadas nesta resposta.

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

Para cada entidade capturada por cópia, um membro de dados não estático não nomeado é declarado no tipo de fechamento. A ordem de declaração desses membros não é especificada. O tipo desse membro de dados é o tipo referenciado, se a entidade for uma referência a um objeto, uma referência lvalue ao tipo de função referenciada, se a entidade for uma referência a uma função ou o tipo da entidade capturada correspondente, caso contrário.

O [m]meio em que a variável min fé capturada por cópia. A entidade mé uma referência ao objeto, portanto, o tipo de fechamento tem um membro cujo tipo é o tipo referenciado. Ou seja, o tipo de membro é inte não int&.

Como o nome mdentro do corpo lambda nomeia o membro do objeto de fechamento e não a variável f(e essa é a parte questionável), a instrução m += 123;modifica esse membro, que é um intobjeto diferente ::n.

aschepler
fonte