[英]std::move behaves differently on different compilers?
我正在試驗一個用於計算余弦相似度的簡單代碼:
#include <iostream>
#include <numeric>
#include <array>
#include <cmath>
float safe_divide(const float& a, const float& b) { return b < 1e-8f && b > -1e-8f ? 0.f : a / b; }
template< size_t N >
float cosine_similarity( std::array<float, N> a, std::array<float, N> b )
{
const float&& a2 = std::move( std::inner_product( a.begin(), a.end(), a.begin(), 0.f ) );
const float&& b2 = std::move( std::inner_product( b.begin(), b.end(), b.begin(), 0.f ) );
const float&& dot_product = std::move( std::inner_product( a.begin(), a.end(), b.begin(), 0.f ) );
return safe_divide( dot_product, ( std::sqrt(a2) * std::sqrt(b2) ) );
}
int main(){
std::array<float, 5> a{1,1,1,1,1}, b{-1,1,-1,1,-1};
std::cout<<cosine_similarity(a,b);
}
在 x86-64 Clang 12.0.1(和其他版本)上,它編譯並給出正確答案。
但是,在我測試過的每個版本的 GCC 上,它都能編譯,但給出了錯誤的答案(或沒有答案)。
它提出了幾個問題:
std::move
的使用是否有效?這是實驗的鏈接: https://godbolt.org/z/KWbMYorrc
發生的事情是:
std::inner_product( a.begin(), a.end(), a.begin(), 0.f )
返回一個臨時變量,其生命周期通常在語句結束時結束std::move( std::inner_product( b.begin(), b.end(), b.begin(), 0.f ) );
是臨時不再直接分配給引用。 相反,它被傳遞給 function ( std::move
) 並且它的生命周期在語句結束時結束。std::move
返回相同的引用,但編譯器本質上並不知道這一點。 std::move
只是一個 function。因此,它不會延長底層臨時文件的生命周期。它似乎與 Clang 一起工作只是僥幸。 您在這里看到的是一個表現出未定義行為的程序。
例如,請參閱此代碼(godbolt: https://godbolt.org/z/nPGxMnrzf ),它在某種程度上反映了您的示例,但包括 output 以顯示對象何時被銷毀:
#include <iostream>
class Foo {
public:
Foo() { std::cout << "Foo was created\n"; }
~Foo() { std::cout << "Foo was destroyed\n"; }
};
Foo getAFoo() {
return Foo();
}
Foo &&doBadThings() {
Foo &&a = std::move(getAFoo());
Foo &&b = std::move(getAFoo());
std::cout << "If Foo objects have been destroyed, a and b are dangling refs...\n";
return std::move(a);
}
int main() {
doBadThings();
}
Output 是:
Foo was created
Foo was destroyed
Foo was created
Foo was destroyed
If Foo objects have been destroyed, a and b are dangling refs...
在這種情況下,Clang 和 Gcc 都產生相同的 output,但這足以證明問題所在。
首先你沒有問的問題:
- 在此代碼中使用移動語義是否有意義?
不。移動float
實際上與復制float
完全相同。 您甚至可以考慮按值傳遞參數,因為按引用傳遞它們不會顯着加快速度(盡管,不相信我,測量)。
#include <iostream>
#include <numeric>
#include <array>
#include <cmath>
float safe_divide(float a, float b) { return b < 1e-8f && b > -1e-8f ? 0.f : a / b; }
template< size_t N >
float cosine_similarity( std::array<float, N> a, std::array<float, N> b )
{
return safe_divide( std::inner_product( a.begin(), a.end(), b.begin(), 0.f ),
std::sqrt(std::inner_product( a.begin(), a.end(), a.begin(), 0.f ))
* std::sqrt(std::inner_product( b.begin(), b.end(), b.begin(), 0.f )) );
}
int main(){
std::array<float, 5> a{1,1,1,1,1}, b{-1,1,-1,1,-1};
std::cout<<cosine_similarity(a,b);
}
在此代碼中,調用inner_product
返回的值已經是臨時值。 無需使用std::move
將它們轉換為右值引用。
- 我對 std::move 的使用是否有效?
實際上,問題不在於直接調用std::move
。 問題是您保留對生命周期在行尾結束的臨時對象的引用。 這里
const float&& a2 = std::move( std::inner_product( a.begin(), a.end(), a.begin(), 0.f ) );
const float&& b2 = std::move( std::inner_product( b.begin(), b.end(), b.begin(), 0.f ) );
const float&& dot_product = std::move( std::inner_product( a.begin(), a.end(), b.begin(), 0.f ) );
這些引用是懸空的。 臨時變量在表達式的末尾不復存在。
- 標准怎么說?
從懸空引用中讀取是未定義的行為。
- 為什么只有 Clang 似乎適用於此編譯器,而沒有其他編譯器?
因為未定義的行為是未定義的。
PS:我故意嘗試使用簡單的語言,那是我能理解和說的語言;)。 價值類別的細節和通過將臨時對象綁定到引用來延長臨時對象的生命周期比這個答案可能暗示的要復雜得多。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.