[英]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 問題 1472和CWG 問題 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.