簡體   English   中英

lambda 函數從全局變量的引用中可變捕獲的行為差異

[英]Behavior difference of lambda function mutable capture from a reference to global variable

我發現如果我使用 lambda 來捕獲對具有 mutable 關鍵字的全局變量的引用,然后修改 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;
}

結果來自 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++ 標准允許這樣做嗎?

lambda 不能通過值捕獲引用本身(為此目的使用std::reference_wrapper )。

在您的 lambda 中, [m]按值捕獲m (因為捕獲中沒有& ),因此首先取消引用m (作為對n引用)並捕獲它所引用的事物的副本n )。 這與這樣做沒有什么不同:

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

然后 lambda 修改該副本,而不是原始副本。 正如預期的那樣,這就是您在 VS 和 GCC 輸出中看到的情況。

Clang 輸出是錯誤的,如果還沒有,應該報告為錯誤。

如果您希望 lambda 修改n ,請改為通過引用捕獲m[&m] 這與將一個引用分配給另一個引用沒有什么不同,例如:

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

或者,您可以完全擺脫m並通過引用捕獲n[&n]

雖然,由於n在全局范圍內,它確實根本不需要被捕獲,但 lambda 可以在不捕獲它的情況下全局訪問它:

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

我認為 Clang 實際上可能是正確的。

根據[lambda.capture]/11 ,僅當它構成odr-use 時,lambda 中使用的id-expression 才指代 lambda 的 by-copy-captured 成員。 如果不是,則它指的是原始實體 這適用於自 C++11 以來的所有 C++ 版本。

根據 C++17 的[basic.dev.odr]/3,如果對引用變量應用左值到右值轉換會產生常量表達式,則不會使用 odr。

然而,在 C++20 草案中,左值到右值轉換的要求被刪除,相關段落多次更改以包含或不包含轉換。 請參閱CWG 問題 1472CWG 問題 1741 ,以及開放的CWG 問題 2083

由於m是用常量表達式(指的是靜態存儲持續時間對象)初始化的,因此使用它會在[expr.const]/2.11.1 中為每個異常生成一個常量表達式。

但是,如果應用了左值到右值的轉換,則情況並非如此,因為n的值在常量表達式中不可用。

因此,取決於是否應該在確定 odr 使用時應用左值到右值轉換,當您在 lambda 中使用m時,它可能會或可能不會指代 lambda 的成員。

如果應該應用轉換,則 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

因為 lambda 中的m將引用閉包的成員,該成員是從f的引用變量m復制初始化的int類型。

這在 C++17 標准中是不允許的,但在其他一些標准草案中可能是這樣。 由於本答案中未解釋的原因,這很復雜。

[expr.prim.lambda.capture]/10

對於副本捕獲的每個實體,在閉包類型中聲明了一個未命名的非靜態數據成員。 這些成員的聲明順序未指定。 如果實體是對對象的引用,則此類數據成員的類型是引用類型;如果實體是對函數的引用,則是對被引用函數類型的左值引用;否則,則是對應的捕獲實體的類型。

[m]表示f中的變量m被復制捕獲。 實體m是對 object 的引用,因此閉包類型有一個成員,其類型是被引用類型。 也就是說,成員的類型是int ,而不是int&

由於 lambda 主體中的名稱m命名了閉包對象的成員而不是f的變量(這是有問題的部分),因此語句m += 123; 修改那個成員,它是一個與::n不同的int對象。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM