std::перемещение ведет себя по-разному в разных компиляторах?

#c #gcc #g #clang

Вопрос:

Я экспериментировал с простым кодом для вычисления косинусного сходства:

 #include <iostream>
#include <numeric>
#include <array>
#include <cmath>

float safe_divide(const floatamp; a, const floatamp; b) { return b < 1e-8f amp;amp; 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 floatamp;amp; a2 = std::move( std::inner_product( a.begin(), a.end(), a.begin(), 0.f ) );
    const floatamp;amp; b2 = std::move( std::inner_product( b.begin(), b.end(), b.begin(), 0.f ) );
    const floatamp;amp; 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

Комментарии:

1. зачем вам std::move вообще здесь понадобилось? Это так r-values .

2. это интересный вопрос для понимания r-значений и std::move , как сказал Марек, вам не нужно ничего из этого в вашем коде. В любом случае, перемещение а float -это всего лишь копия

3. std::inner_product is returning an l-value Су, ты думаешь, что сможешь std::inner_product(...) = something; ?

4. Возвращаемое значение из любой функции никогда не требуется std::move (даже если оно возвращает ссылку на перемещение).

5. Так, как вы написали, компилятор создал временный объект типа float , и вы держите ссылку на него дольше, чем его срок службы, что приводит к UB.

Ответ №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, — это просто случайность. То, что у вас здесь есть, — это программа, демонстрирующая неопределенное поведение.

См., например, этот код (godbolt: https://godbolt.org/z/nPGxMnrzf), который в некоторой степени отражает ваш пример, но включает выходные данные, чтобы показать, когда объекты уничтожаются:

 #include <iostream>

class Foo {
    public:
    Foo() { std::cout << "Foo was createdn"; }
    ~Foo() { std::cout << "Foo was destroyedn"; }
};

Foo getAFoo() {
    return Foo();
}

Foo amp;amp;doBadThings() {
    Foo amp;amp;a = std::move(getAFoo());
    Foo amp;amp;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. Спасибо вам за лаконичный ответ. Я почти собирался неправильно понять, что проблема заключалась в приведении r-значения к r-значению ( что не должно быть проблемой в соответствии с правилами приведения ). Теперь я вижу, что это было временное явление, которое передавалось без какого-либо продления его жизни.

2. @Тарсалис прав. Кстати, если вы std::move замените фактическое приведение к ссылке на значение r ( static_cast<float amp;amp;> ), проблема исчезнет.

Ответ №2:

Во-первых, вопрос, который вы не задавали:

  1. Имеет ли смысл использовать семантику перемещения в этом коде?

Нет. Перемещение a float на самом деле в точности совпадает с копированием a float . Вы могли бы даже подумать о передаче параметров по значению, потому что передача их по ссылке существенно не ускорит процесс (хотя, не поверьте мне, измерьте).

 #include <iostream>
#include <numeric>
#include <array>
#include <cmath>

float safe_divide(float a, float b) { return b < 1e-8f amp;amp; 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 их для приведения к ссылкам с r-значением.

  1. Допустимо ли мое использование std::move вообще?

На самом деле проблема заключается не в прямом вызове std::move , а в том, что это проблема. Проблема в том, что вы сохраняете ссылки на временные файлы, срок службы которых заканчивается в конце строки. Здесь

 const floatamp;amp; a2 = std::move( std::inner_product( a.begin(), a.end(), a.begin(), 0.f ) );
const floatamp;amp; b2 = std::move( std::inner_product( b.begin(), b.end(), b.begin(), 0.f ) );
const floatamp;amp; dot_product = std::move( std::inner_product( a.begin(), a.end(), b.begin(), 0.f ) );
 

Эти ссылки болтаются. Временные значения перестают существовать в конце выражений.

  1. Что говорится в стандарте?

Чтение из висячей ссылки-это неопределенное поведение.

  1. Почему только Clang, кажется, работает с этим и никаким другим компилятором?

Потому что неопределенное поведение не определено.

PS: Намеренно я старался использовать простой язык, это тот язык, который я могу понять и на котором говорю ;). Детали категорий ценностей и продление срока службы временных объектов путем привязки их к ссылкам более сложны, чем может показаться из этого ответа.