Неожиданный результат с `std::views::transform`, вероятно, вызван "безымянным временным"

Следующий код (божий болт):

#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?

Без -O3 нет вообще нет проблем.

康桓瑋 17.06.2024 03:55

Меньший MRE: gcc.godbolt.org/z/44bz9o1Ga

Weijun Zhou 17.06.2024 04:11
&S::x при вызове возвращает ссылку.
n. m. could be an AI 17.06.2024 07:50

@康桓瑋 Просто добавьте -fsanitize=адрес.

n. m. could be an AI 17.06.2024 07:51
std::invokeing &S::x возвращается по ссылке, а ваша лямбда возвращается по значению. Я не уверен, почему первое является незаконным, но, похоже, в этом и есть разница.
HolyBlackCat 17.06.2024 08:30
[] ( 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
Fedor 17.06.2024 08:31

@Федор, я до сих пор не понимаю, почему это УБ, если это (float t:...), а не (const float& t:...).

Weijun Zhou 17.06.2024 08:34

@WeijunZhou, временное S уничтожается до того, как ссылка на его поле будет доступна и скопирована float t :

Fedor 17.06.2024 08:39

Я думаю, что цикл диапазона создает временное получение, начало и уничтожает временное. Это ожидаемое поведение в C++ до 26. в язык были внесены изменения, позволяющие использовать его в C++26.

alfC 17.06.2024 08:47

@alfC Clang багажник с __cpp_range_based_for==202211L все еще имеет проблему, если вы об этом.

Weijun Zhou 17.06.2024 08:52
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
10
216
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

S::f() возвращает временный файл из его подписи; S f() const;. Использование ссылки на переменную-член этого временного объекта является неопределенным поведением.

С другой стороны, получение значения переменной-члена временного объекта — это четко определенное поведение, поэтому это совершенно нормально:

std::views::transform( [] ( const S& l ) { return l.x; } );

Лямбда возвращается по значению. До C++23 выражением временного диапазона было UB.

Простое решение — изменить подпись, чтобы она возвращалась по ссылке lvalue; const S& f() const;. Тогда все остальное будет работать корректно.

Почему принимать const T& безопаснее? Продлевает ли это жизнь временщикам здесь?

Fedor 17.06.2024 08:24

Хм? Формирование указателя на временный объект приведет к ошибке компиляции, а не UB. И ОП этого не делает, они применяют указатель члена к временному, что само по себе является законным.

HolyBlackCat 17.06.2024 08:25

Указатель участника @HolyBlackCat? Я что-то упускаю, не так ли? С++ :)

Chukwujiobi Canon 17.06.2024 08:27
&S::x — это указатель на член. На первый взгляд кажется, что код ОП делает это: S() .* &S::x, что само по себе является законным. (Но если вы присвоите результат ссылке, он может болтаться.)
HolyBlackCat 17.06.2024 08:29

Вы можете изменить (const S& l) в лямбде на (S l), и дезинфицирующее средство не будет жаловаться.

Weijun Zhou 17.06.2024 08:31

@HolyBlackCat И в конечном итоге ему будет присвоена ссылка, потому что… (Я не уверен, но я предполагаю, что так и будет, потому что результатом std::views::transform в конечном итоге будет представление.

Chukwujiobi Canon 17.06.2024 08:32

Ммм, но это не то объяснение, которое вы даете в своем ответе. :)

HolyBlackCat 17.06.2024 08:37

Ты покажешь мне мою ошибку, мой друг. На самом деле сегодня ты научил меня чему-то новому. Спасибо.

Chukwujiobi Canon 17.06.2024 08:45

До C++23 выражением временного диапазона было UB. Ссылаться на это нехорошо. Я добавлю это для полноты картины.

Chukwujiobi Canon 17.06.2024 09:29

@HolyBlackCat еще раз спасибо. Это займет довольно много времени, но я обещаю, что стану лучше в этом языке.

Chukwujiobi Canon 17.06.2024 09:45
Ответ принят как подходящий

Ваш пример по сути эквивалентен примеру, приведенному в LWG 3502.

Поскольку разыменование views::transform(&S::f) дает prvalue, views::transform(&S::x) создаст ссылку на материализованное временное пространство, которое станет висячим, как только operator* вернется, как описано в исходном выпуске.

Другие вопросы по теме