参照からグローバル変数へのラムダ関数の可変キャプチャの動作の違い


22

ラムダを使用して、可変キーワードでグローバル変数への参照をキャプチャし、ラムダ関数の値を変更すると、コンパイラによって結果が異なることがわかりました。

#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;
}

VS 2015およびGCCからの結果(g ++(Ubuntu 5.4.0-6ubuntu1〜16.04.12)5.4.0 20160609):

100 223 100

clang ++からの結果(clangバージョン3.8.0-2ubuntu4(tags / RELEASE_380 / final)):

100 223 223

なぜこれが起こるのですか?これはC ++標準で許可されていますか?


Clangの動作は引き続きトランクに存在します。
クルミ

これらはすべてかなり古いコンパイラバージョンです
MM

それはまだClangの最近のバージョンにあります:godbolt.org/z/P9na9c
ウィリー

1
キャプチャを完全に削除しても、GCCはこのコードを受け入れ、clangが行うことを実行します。これは、GCCバグがあることを強く示唆しています。単純なキャプチャはラムダ本体の意味を変更するものではありません。
TC

回答:


16

ラムダは参照自体を値でキャプチャすることはできません(std::reference_wrapperそのために使用します)。

ラムダでは、値で[m]キャプチャmします(キャプチャにはないため&)。したがってm(への参照であるためn)最初に逆参照され、参照しているもの()のコピーnがキャプチャされます。これはこれを行うことと同じです:

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

次に、ラムダは元のコピーではなく、そのコピーを変更します。期待どおり、VSおよびGCC出力でこれが発生しています。

Clangの出力は正しくありません。バグがまだ報告されていない場合は、バグとして報告する必要があります。

ラムダを変更する場合はnm代わりに参照でキャプチャします[&m]。これは、ある参照を別の参照に割り当てることと同じです。

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

または、代わりにm完全に削除してn、参照でキャプチャすることもできます[&n]

以来、がnグローバルスコープである、それは本当にすべてでキャプチャする必要はありません、ラムダは、それをキャプチャすることなく、グローバルにアクセスすることができます。

return [] () -> int {
    n += 123;
    return n;
};

5

Clangは実際には正しいと思う。

[lambda.capture] / 11によれば、ラムダで使用されるid式は、それがodr-useを構成する場合にのみ、ラムダのby-copy-capturedメンバーを参照します。そうでない場合は、元のエンティティを参照します。これは、C ++ 11以降のすべてのC ++バージョンに適用されます。

C ++ 17の[basic.dev.odr] / 3によると、参照変数にlvalueからrvalueへの変換を適用して定数式が生成される場合、参照変数はodrで使用されません。

ただし、C ++ 20ドラフトでは、左辺値から右辺値への変換の要件が削除され、関連する節が変換を含めるか含めないように複数回変更されました。CWG課題1472およびCWG課題1741、およびオープンCWG課題2083を参照してください。

mは定数式(静的ストレージ期間オブジェクトを参照)で初期化されるため、これを使用すると、[expr.const] /2.11.1の例外ごとに定数式が生成されます。

ただし、左辺値から右辺値への変換が適用される場合、これは当てはまりnません。なぜなら、の値は定数式では使用できないためです。

したがって、使用するときに、odr-useを決定する際に左辺値から右辺値への変換が適用されることになっているかどうかによって、 m、ラムダで、ラムダのメンバーを参照する場合としない場合があります。

変換を適用する必要がある場合は、GCCとMSVCが正しく、そうでない場合はClangが適用されます。

の初期化をm定数式ではないように変更すると、Clangが動作を変更することがわかります。

#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;
}

この場合、すべてのコンパイラは出力が

100 223 100

ので、mラムダがタイプである閉鎖のメンバーを参照するにはint、参照変数からコピー初期化mの中でf


VS / GCCとClangの両方の結果は正しいですか?またはそのうちの1つだけですか?
ウィリー

[basic.dev.odr] / 3は、m左辺値から右辺値への変換を適用するのが定数式である場合を除いて、変数がそれを命名する式によってodr使用されることを示しています。[expr.const] /(2.7)によって、その変換はコア定数式ではなくなります。
aschepler

Clangの結果が正しければ、なんとなく直観に反すると思います。プログラマーの観点からは、キャプチャー・リストに書き込む変数が実際に変更可能な場合にコピーされることを確認する必要があるため、mの初期化は、何らかの理由で後でプログラマーによって変更される可能性があります。
ウィリー

1
m += 123;こちらmがodr-usedです。
Oliv

1
Clangは現在の言い回しで正しいと思います。私はこれについて詳しく調べていませんが、関連する変更はほぼすべてのDRです。
TC

4

これはC ++ 17標準では許可されていませんが、他の標準ドラフトでは許可されている場合があります。この回答で説明されていない理由により、それは複雑です。

[expr.prim.lambda.capture] / 10

コピーによってキャプチャされたエンティティごとに、名前のない非静的データメンバーがクロージャタイプで宣言されます。これらのメンバーの宣言順序は指定されていません。このようなデータメンバーの型は、エンティティがオブジェクトへの参照である場合は参照型、エンティティが関数への参照である場合は参照される関数型への左辺値参照、その他の場合は対応するキャプチャされたエンティティの型です。

[m]変数という意味mでは、fコピーによって捕獲されます。エンティティmはオブジェクトへの参照であるため、クロージャタイプには、参照されるタイプのタイプのメンバーがあります。つまり、メンバーのタイプはintであり、int&

名前以来mラムダ本体名閉鎖オブジェクトのメンバーではなく、変数で内側f(これは疑わしい部分である)、ステートメントm += 123;異なるメンバー、修正intからオブジェクト::n

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.