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. 标准怎么说?

这里有一个 link 实验:https://godbolt.org/z/KWbMYorrc

发生的事情是:

  • std::inner_product( a.begin(), a.end(), a.begin(), 0.f ) returns 临时的,其生命周期通常在语句结束时结束
  • 当您将临时对象直接分配给引用时,有一个特殊规则可以延长临时对象的生命周期
  • 但是,std::move( std::inner_product( b.begin(), b.end(), b.begin(), 0.f ) ); 的问题是临时对象不再直接分配给引用。相反,它被传递给一个函数(std::move)并且它的生命周期在语句结束时结束。
  • std::move returns 相同的引用,但编译器本质上并不知道这一点。 std::move 只是一个函数。因此,它不会延长底层临时文件的生命周期。

它似乎与 Clang 一起工作只是侥幸。你这里有一个展示 undefined behaviour.

的程序

例如,请参阅此代码 (godbolt: https://godbolt.org/z/nPGxMnrzf),它在某种程度上反映了您的示例,但包括显示对象何时被销毁的输出:

#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();
}

输出为:

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 都产生相同的输出,但这足以说明问题。

首先你没问的问题:

  1. Does it make sense to use move semantics in this code?

没有。移动一个 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. Is my use of std::move even valid?

实际上问题不在于直接调用 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. What does the standard say?

读取悬空引用是未定义的行为。

  1. Why does only Clang seem to work with this and no other compiler?

因为未定义的行为是未定义的。

PS:我故意尝试使用简单的语言,那是我能理解和说的语言;)。价值类别的细节和通过将它们绑定到引用来延长临时对象的生命周期比这个答案可能暗示的要复杂得多。