簡體   English   中英

std::move 在不同的編譯器上表現不同?

[英]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 上,它都能編譯,但給出了錯誤的答案(或沒有答案)。

它提出了幾個問題:

  1. 我對std::move的使用是否有效?
  2. 為什么只有 Clang 似乎適用於此編譯器,而沒有其他編譯器?
  3. 標准怎么說?

這是實驗的鏈接: 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,但這足以證明問題所在。

首先你沒有問的問題:

  1. 在此代碼中使用移動語義是否有意義?

不。移動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將它們轉換為右值引用。

  1. 我對 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 ) );

這些引用是懸空的。 臨時變量在表達式的末尾不復存在。

  1. 標准怎么說?

從懸空引用中讀取是未定義的行為。

  1. 為什么只有 Clang 似乎適用於此編譯器,而沒有其他編譯器?

因為未定義的行為是未定義的。

PS:我故意嘗試使用簡單的語言,那是我能理解和說的語言;)。 價值類別的細節和通過將臨時對象綁定到引用來延長臨時對象的生命周期比這個答案可能暗示的要復雜得多。

暫無
暫無

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

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