#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, которую я тестировал, она компилируется, но дает неправильный ответ (или не дает ответа).
Это вызывает несколько вопросов:
- Является ли мое использование
std::move
даже допустимым? - Почему только Clang, кажется, работает с этим и никаким другим компилятором?
- Что говорится в стандарте?
Вот ссылка на эксперимент: 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:
Во-первых, вопрос, который вы не задавали:
- Имеет ли смысл использовать семантику перемещения в этом коде?
Нет. Перемещение 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-значением.
- Допустимо ли мое использование 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 ) );
Эти ссылки болтаются. Временные значения перестают существовать в конце выражений.
- Что говорится в стандарте?
Чтение из висячей ссылки-это неопределенное поведение.
- Почему только Clang, кажется, работает с этим и никаким другим компилятором?
Потому что неопределенное поведение не определено.
PS: Намеренно я старался использовать простой язык, это тот язык, который я могу понять и на котором говорю ;). Детали категорий ценностей и продление срока службы временных объектов путем привязки их к ссылкам более сложны, чем может показаться из этого ответа.