Следующий код (божий болт):
#include <iostream>
#include <vector>
#include <ranges>
struct S
{
float x = 150.f;
S f() const
{
return *this;
}
};
int main()
{
std::vector<S> vec{
{ 1.f },
{ 2.f }
};
std::cout << "\nCreated\n";
for ( const auto& l : vec )
{
std::cout << l.f().x << ' ';
}
std::cout << "\nView0\n";
for ( float t : vec
| std::views::transform( &S::f )
| std::views::transform( &S::x )
)
{
std::cout << t << ' ';
}
std::cout << "\nView1\n";
auto view1
= vec
| std::views::transform( &S::f )
| std::views::transform( [] ( const S& l ) { return l.x; } );
for ( float t : view1 )
{
std::cout << t << ' ';
}
}
выдает следующий результат (для Clang и GCC с включенной оптимизацией):
Created
1 2
View0
0 0
View1
1 2
Я обнаружил, что нули не фиксированы, получение вывода View0 выглядит как неопределенное поведение, однако я не понимаю, почему.
Другое наблюдение заключается в том, что включение -Wall -Wextra -pedantic-errors в GCC приводит к появлению некоторых предупреждений:
<source>:33:52: warning: using a dangling pointer to an unnamed temporary [-Wdangling-pointer=]
33 | | std::views::transform( &S::x )
| ^
In file included from <source>:3:
/opt/compiler-explorer/gcc-14.1.0/include/c++/14.1.0/ranges:1949:54: note: unnamed temporary defined here
1949 | { return std::__invoke(*_M_parent->_M_fun, *_M_current); }
<source>:33:52: warning: '<unnamed>.S::x' may be used uninitialized [-Wmaybe-uninitialized]
33 | | std::views::transform( &S::x )
| ^
/opt/compiler-explorer/gcc-14.1.0/include/c++/14.1.0/ranges:1949:54: note: '<anonymous>' declared here
1949 | { return std::__invoke(*_M_parent->_M_fun, *_M_current); }
Мой вопрос: почему результат после View0 не равен результату после Created и View1?
Меньший MRE: gcc.godbolt.org/z/44bz9o1Ga
&S::x при вызове возвращает ссылку.
@康桓瑋 Просто добавьте -fsanitize=адрес.
std::invokeing &S::x возвращается по ссылке, а ваша лямбда возвращается по значению. Я не уверен, почему первое является незаконным, но, похоже, в этом и есть разница.
[] ( const S& l ) { return l.x; } возвращает безопасное значение. И std::views::transform( &S::x ) использует временную ссылку, такую же, как [] ( const S& l ) -> const float& { return l.x; }, то есть UB: gcc.godbolt.org/z/9951nT486@Федор, я до сих пор не понимаю, почему это УБ, если это (float t:...), а не (const float& t:...).
@WeijunZhou, временное S уничтожается до того, как ссылка на его поле будет доступна и скопирована float t :
Я думаю, что цикл диапазона создает временное получение, начало и уничтожает временное. Это ожидаемое поведение в C++ до 26. в язык были внесены изменения, позволяющие использовать его в C++26.
@alfC Clang багажник с __cpp_range_based_for==202211L все еще имеет проблему, если вы об этом.





S::f() возвращает временный файл из его подписи; S f() const;. Использование ссылки на переменную-член этого временного объекта является неопределенным поведением.
С другой стороны, получение значения переменной-члена временного объекта — это четко определенное поведение, поэтому это совершенно нормально:
std::views::transform( [] ( const S& l ) { return l.x; } );Лямбда возвращается по значению. До C++23 выражением временного диапазона было UB.
Простое решение — изменить подпись, чтобы она возвращалась по ссылке lvalue; const S& f() const;. Тогда все остальное будет работать корректно.
Почему принимать const T& безопаснее? Продлевает ли это жизнь временщикам здесь?
Хм? Формирование указателя на временный объект приведет к ошибке компиляции, а не UB. И ОП этого не делает, они применяют указатель члена к временному, что само по себе является законным.
@Fedor См. Время жизни временных файлов явно привязано к ссылкам
Указатель участника @HolyBlackCat? Я что-то упускаю, не так ли? С++ :)
&S::x — это указатель на член. На первый взгляд кажется, что код ОП делает это: S() .* &S::x, что само по себе является законным. (Но если вы присвоите результат ссылке, он может болтаться.)
Вы можете изменить (const S& l) в лямбде на (S l), и дезинфицирующее средство не будет жаловаться.
@HolyBlackCat И в конечном итоге ему будет присвоена ссылка, потому что… (Я не уверен, но я предполагаю, что так и будет, потому что результатом std::views::transform в конечном итоге будет представление.
Ммм, но это не то объяснение, которое вы даете в своем ответе. :)
Ты покажешь мне мою ошибку, мой друг. На самом деле сегодня ты научил меня чему-то новому. Спасибо.
До C++23 выражением временного диапазона было UB. Ссылаться на это нехорошо. Я добавлю это для полноты картины.
@HolyBlackCat еще раз спасибо. Это займет довольно много времени, но я обещаю, что стану лучше в этом языке.
Ваш пример по сути эквивалентен примеру, приведенному в LWG 3502.
Поскольку разыменование views::transform(&S::f) дает prvalue, views::transform(&S::x) создаст ссылку на материализованное временное пространство, которое станет висячим, как только operator* вернется, как описано в исходном выпуске.
Без
-O3нет вообще нет проблем.