【问题标题】:std::move behaves differently on different compilers?std::move 在不同的编译器上表现不同?
【发布时间】:2021-11-22 04:53:24
【问题描述】:

我正在试验一个计算余弦相似度的简单代码:

#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. 我对@9​​87654323@ 的使用是否有效?
  2. 为什么似乎只有 Clang 可以使用这个编译器而没有其他编译器?
  3. 标准是怎么说的?

这里是实验的链接:https://godbolt.org/z/KWbMYorrc

【问题讨论】:

  • 这里为什么需要std::move?那些是r-values
  • 这是一个了解 r 值和std::move 的有趣问题,但正如 Marek 所说,您的代码中不需要任何这些。无论如何,移动float 只是一个副本
  • std::inner_product is returning an l-value Soo,你觉得你可以std::inner_product(...) = something;吗?
  • 任何函数的返回值都不需要std::move(即使它返回移动引用)。
  • 按照您编写它的方式,编译器创建了 float 类型的临时对象,并且您对它的引用时间长于导致 UB 的生命周期。

标签: c++ gcc g++ clang


【解决方案1】:

正在发生的事情是:

  • std::inner_product( a.begin(), a.end(), a.begin(), 0.f ) 返回一个临时的,其生命周期通常在语句结束时结束
  • 当您将临时对象直接分配给引用时,有一条特殊规则可以延长临时对象的使用寿命
  • 然而,std::move( std::inner_product( b.begin(), b.end(), b.begin(), 0.f ) ); 的问题是临时不再直接分配给引用。相反,它被传递给一个函数 (std::move),它的生命周期在语句结束时结束。
  • std::move 返回相同的引用,但编译器本质上并不知道这一点。 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 都产生相同的输出,但足以证明问题。

【讨论】:

  • 感谢您的简洁回答。我几乎要误解问题是将 r 值转换为 r 值(根据转换规则这不应该是问题)。我现在明白了,它只是暂时的被传递,没有任何东西可以延长它的寿命。
  • @Tharsalys 对。顺便说一句,如果您将 std::move 替换为实际转换为 r 值引用 (static_cast&lt;float &amp;&amp;&gt;),问题就会消失。
【解决方案2】:

首先是你没有问的问题:

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

【讨论】:

    猜你喜欢
    • 2010-11-28
    • 1970-01-01
    • 2013-02-17
    • 1970-01-01
    • 2021-12-10
    • 1970-01-01
    • 2015-12-13
    • 2022-10-16
    • 2019-09-10
    相关资源
    最近更新 更多