[英]Is it safe to make a const reference member to a temporary variable?
我試過多次這樣編碼:
struct Foo
{
double const& f;
Foo(double const& fx) : f(fx)
{
printf("%f %f\n", fx, this->f); // 125 125
}
double GetF() const
{
return f;
}
};
int main()
{
Foo p(123.0 + 2.0);
printf("%f\n", p.GetF()); // 0
return 0;
}
但它根本不會崩潰。 我還使用valgrind來測試程序,但沒有出現錯誤或警告。 所以,我假設編譯器自動生成了一個代碼,將引用指向另一個隱藏變量。 但我真的不確定。
不,這不安全。 更准確地說,這是UB ,意味着一切皆有可能。
當您將123.0 + 2.0
傳遞給Foo
的構造函數時,將構造一個臨時double
123.0 + 2.0
並將其綁定到參數fx
。 在完整表達式(即Foo p(123.0 + 2.0);
)之后臨時將被銷毀,然后引用成員f
將變為懸空。
請注意, 臨時對象的生命周期不會延長到引用成員f
的生命周期。
通常,不能通過“傳遞”來進一步延長臨時文件的生命周期:從臨時文件綁定的引用初始化的第二個引用不會影響其生命周期。
從標准, [class.base.init]/8
綁定到內存初始值設定項中的引用成員的臨時表達式格式錯誤。 [ 例子:
struct A { A() : v(42) { } // error const int& v; };
— 結束示例 ]
但它根本不會崩潰。 我還使用 valgrind 來測試程序,但沒有出現錯誤或警告。
啊,調試未定義行為的樂趣。 編譯器可能會將無效代碼編譯為工具無法再檢測到無效代碼的內容,這就是這里發生的情況。
從操作系統的角度來看,從 valgrind 的角度來看, f
引用的內存仍然有效,因此它不會崩潰,並且 valgrind 不會報告任何錯誤。 您看到輸出值為0
的事實意味着編譯器在您的情況下重新使用了以前用於臨時對象的內存來存儲其他一些不相關的值。
應該清楚的是,通過對已刪除對象的引用來訪問該不相關值的嘗試是無效的。
是的,只要引用僅在“臨時”變量的生命周期尚未結束時使用。 在您發布的代碼中,您在引用對象的生命周期后一直保持引用。 (即不好)
不,這不是正在發生的事情。
在我的機器上,你的主打印語句打印 125 而不是 0,所以首先讓我們復制你的結果:
#include <alloca.h>
#include <cstring>
#include <iostream>
struct Foo
{
double const& f;
Foo(double const& fx) : f(fx)
{
std::cout << fx << " " << this->f << std::endl;
}
double GetF() const
{
return f;
}
};
Foo make_foo()
{
return Foo(123.0 + 2.0);
}
int main()
{
Foo p = make_foo();
void * const stack = alloca(1024);
std::memset(stack, 0, 1024);
std::cout << p.GetF() << std::endl;
return 0;
}
現在它打印 0!
125.0 和 2.0 是浮點文字。 它們的和一個右值是物化的Foo對象的施工過程中,由於Foo的構造函數需要一個雙重的參考。 該臨時雙精度存在於堆棧的內存中。
引用通常被實現來保存它們引用的對象的機器地址,這意味着 Foo 的引用成員保存着一個堆棧內存地址。 調用 Foo 的構造函數時存在於該地址的對象,在構造函數完成后不存在。
在我的機器上,當臨時的生命周期結束時,堆棧內存不會自動清零,因此在您的代碼中,引用返回(前)對象的值。 在我的代碼中,當我重用之前由臨時(通過 alloca 和 memset)占用的堆棧內存時,該內存被(正確)覆蓋,並且引用的未來使用反映了地址處的內存狀態,該地址不再有任何與臨時的關系。 在這兩種情況下,內存地址都是有效的,因此不會觸發段錯誤。
由於某些特定於編譯器的行為,我添加了 make_foo 並使用了 alloca 和 std::memset,因此我可以使用直觀的名稱“stack”,但我可以同樣輕松地完成此操作,從而獲得類似的結果:
Foo p = Foo(123.0 + 2.0);
std::vector<unsigned char> v(1024, 0);
std::cout << p.GetF() << std::endl;
這確實是不安全的(它具有未定義的行為),並且 asan AddressSanitizerUseAfterScope將檢測到這一點:
$ g++ -ggdb3 a.cpp -fsanitize=address -fsanitize-address-use-after-scope && ./a.out
125.000000 125.000000
=================================================================
==11748==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7fff1bbfdab0 at pc 0x000000400b80 bp 0x7fff1bbfda20 sp 0x7fff1bbfda18
READ of size 8 at 0x7fff1bbfdab0 thread T0
#0 0x400b7f in Foo::GetF() const a.cpp:12
#1 0x4009ca in main a.cpp:18
#2 0x7fac0bd05d5c in __libc_start_main (/lib64/libc.so.6+0x1ed5c)
#3 0x400808 (a.out+0x400808)
Address 0x7fff1bbfdab0 is located in stack of thread T0 at offset 96 in frame
#0 0x4008e6 in main a.cpp:16
This frame has 2 object(s):
[32, 40) 'p'
[96, 104) '<unknown>' <== Memory access at offset 96 is inside this variable
為了使用 AddressSanitizerUseAfterScope,您需要運行 Clang 5.0 或 gcc 7.1。
Valgrind 擅長檢測堆內存的無效使用,但由於它運行在未更改的程序文件上,因此通常無法檢測堆棧使用錯誤。
您的代碼是不安全的,因為參數double const& fx
綁定到一個臨時的、具有值 125.0 的物化純右值 double。 這個臨時的生命周期在語句表達式Foo p(123.0 + 2.0)
的末尾終止。
使您的代碼安全的一種方法是使用聚合生命周期擴展( 通過右值數據成員擴展臨時的生命周期適用於聚合,但不適用於構造函數,為什么? ),通過刪除構造函數Foo::Foo(double const&)
,並更改p
的初始值設定項以使用列表初始化語法:
Foo p{123.0 + 2.0};
// ^ ^
如果臨時變量存在於使用引用的位置,則行為定義良好。 在這種情況下,這個臨時變量的存在正是因為它被引用了! 表格 C++11 標准第 12.2.5 節:
引用綁定到的臨時對象或作為引用綁定到的子對象的完整對象的臨時對象在引用的生命周期內持續存在......
是的,被 '...' 隱藏的詞是“except”,那里列出了多個例外,但在這個例子中沒有一個是適用的。 因此,這是合法且定義明確的,不應產生警告,但不是廣為人知的極端情況。
如果臨時變量存在於使用引用的位置,則行為定義良好。
如果在使用引用之前臨時停止存在,則使用引用的行為是未定義的。
不幸的是,您的代碼是后者的一個例子。 當語句Foo p(123.0 + 2.0)
結束時,保存123.0 + 2.0
結果的臨時對象不復存在。 下一個語句printf("%f\\n", p.GetF())
然后訪問對不再存在的臨時對象的引用。
一般來說,未定義的行為被認為是不安全的——這意味着對代碼實際做什么沒有要求。 不能保證您在測試中看到的結果。
正如其他人所說,它目前是不安全的。 所以應該在編譯時檢查它。 因此,當存儲引用時,還應該禁止右值:
Foo(double &&)=delete;
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.