Différence de comportement de la capture mutable de la fonction lambda d'une référence à une variable globale

22

J'ai trouvé que les résultats sont différents d'un compilateur à l'autre si j'utilise un lambda pour capturer une référence à une variable globale avec un mot clé mutable, puis modifie la valeur dans la fonction 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;
}

Résultat de VS 2015 et GCC (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

Résultat de clang ++ (clang version 3.8.0-2ubuntu4 (tags / RELEASE_380 / final)):

100 223 223

Pourquoi cela arrive-t-il? Est-ce autorisé par les normes C ++?

Willy
la source
Le comportement de Clang est toujours présent sur le coffre.
noyer
Ce sont toutes des versions de compilateur plutôt anciennes
MM
Il présente toujours sur la version récente de Clang: godbolt.org/z/P9na9c
Willy
1
Si vous supprimez entièrement la capture, GCC accepte toujours ce code et fait ce que clang fait. C'est un indice fort qu'il y a un bug GCC - les captures simples ne sont pas censées changer la signification du corps lambda.
TC

Réponses:

16

Un lambda ne peut pas capturer une référence elle-même par valeur (à utiliser std::reference_wrapperà cette fin).

Dans votre lambda, [m]capture mpar valeur (car il n'y a pas& en a dans la capture), donc m(étant une référence à n) est d'abord déréférencé et une copie de la chose à laquelle il fait référence ( n) est capturée. Ce n'est pas différent de faire ceci:

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

Le lambda modifie alors cette copie, pas l'original. C'est ce que vous voyez se produire dans les sorties VS et GCC, comme prévu.

La sortie Clang est incorrecte et doit être signalée comme un bug, si ce n'est pas déjà fait.

Si vous souhaitez modifier votre lambda n , la capture à la mplace par référence: [&m]. Ce n'est pas différent que d'attribuer une référence à une autre, par exemple:

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

Ou, vous pouvez simplement se débarrasser de mtout à fait et la capture à la nplace par référence: [&n].

Bien que, nétant de portée mondiale, il n'a vraiment pas besoin d'être capturé du tout, le lambda peut y accéder globalement sans le capturer:

return [] () -> int {
    n += 123;
    return n;
};
Rémy Lebeau
la source
5

Je pense que Clang peut être correct.

Selon [lambda.capture] / 11 , une expression id utilisée dans le lambda fait référence au membre capturé par copie de lambda uniquement si elle constitue une utilisation odr . Si ce n'est pas le cas, cela fait référence à l' entité d'origine . Cela s'applique à toutes les versions C ++ depuis C ++ 11.

Selon [basic.dev.odr] / 3 de C ++ 17, une variable de référence n'est pas odr utilisée si l'application de la conversion lvalue-to-rvalue lui donne une expression constante.

Dans le projet C ++ 20, cependant, l'exigence de conversion lvalue-to-rvalue est supprimée et le passage correspondant a été modifié plusieurs fois pour inclure ou non la conversion. Voir CWG numéro 1472 et CWG numéro 1741 , ainsi que CWG numéro 2083 ouvert .

Puisque mest initialisé avec une expression constante (faisant référence à un objet de durée de stockage statique), son utilisation donne une expression constante par exception dans [expr.const] /2.11.1 .

Ce n'est cependant pas le cas si des conversions lvalue-to-rvalue sont appliquées, car la valeur de nn'est pas utilisable dans une expression constante.

Par conséquent, selon que les conversions lvalue-to-rvalue doivent ou non être appliquées pour déterminer l'utilisation odr, lorsque vous utilisez m dans le lambda, cela peut ou non se référer au membre du lambda.

Si la conversion doit être appliquée, GCC et MSVC sont corrects, sinon Clang l'est.

Vous pouvez voir que Clang change son comportement si vous changez l'initialisation de mpour ne plus être une expression 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;
}

Dans ce cas, tous les compilateurs conviennent que la sortie est

100 223 100

car mdans le lambda fera référence au membre de la fermeture qui est de type intinitialisé à la copie à partir de la variable de référence mdans f.

noyer
la source
Les résultats de VS / GCC et Clang sont-ils corrects? Ou un seul d'entre eux?
Willy
[basic.dev.odr] / 3 indique que la variable mest utilisée par odr par une expression la nommant, sauf si lui appliquer la conversion lvalue-à-rvalue serait une expression constante. Par [expr.const] / (2.7), cette conversion ne serait pas une expression constante de base.
aschepler
Si le résultat de Clang est correct, je pense que c'est en quelque sorte contre-intuitif. Parce que du point de vue du programmeur, il doit s'assurer que la variable qu'il écrit dans la liste de capture est réellement copiée pour le cas modifiable, et l'initialisation de m peut être modifiée par le programmeur plus tard pour une raison quelconque.
Willy
1
m += 123;Voici modr-utilisé.
Oliv
1
Je pense que Clang a raison par le libellé actuel, et même si je n'y ai pas creusé, les changements pertinents ici sont presque certainement tous les DR.
TC
4

Ce n'est pas autorisé par la norme C ++ 17, mais par certains autres projets de norme, cela pourrait l'être. C'est compliqué, pour des raisons non expliquées dans cette réponse.

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

Pour chaque entité capturée par copie, un membre de données non statique non nommé est déclaré dans le type de fermeture. L'ordre de déclaration de ces membres n'est pas précisé. Le type d'un tel membre de données est le type référencé si l'entité est une référence à un objet, une référence lvalue au type de fonction référencé si l'entité est une référence à une fonction, ou le type de l'entité capturée correspondante dans le cas contraire.

Les [m]moyens que la variable men fest capturé par copie. L'entité mest une référence à un objet, donc le type de fermeture a un membre dont le type est le type référencé. Autrement dit, le type de membre est int, et non int&.

Étant donné que le nom mà l'intérieur du corps lambda nomme le membre de l'objet de fermeture et non la variable dans f(et c'est la partie discutable), l'instruction m += 123;modifie ce membre, qui est un intobjet différent de ::n.

aschepler
la source