Unexpected result with `std::views::transform`, probably caused by "unnamed temporary"
Asked Answered
B

2

8

The following code (godbolt):

#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 << ' ';
    }
}

produces the following output (for Clang and GCC with optimizations enabled):

Created
1 2 
View0
0 0 
View1
1 2

I've found that the zeros are not fixed, getting the output of View0 looks like undefined behavior, however I fail to see why.

Another observation is that enabling -Wall -Wextra -pedantic-errors in GCC causes some warnings to appear:

<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); }

My question is: Why is the output after View0 not equal to the output after Created and View1?

Behlau answered 16/6 at 22:0 Comment(10)
Without -O3 there is no problem at all.Welladvised
A smaller MRE: gcc.godbolt.org/z/44bz9o1GaCassondracassoulet
&S::x, when invoked, returns a reference.Intermarry
@康桓瑋 Just add -fsanitize=address.Intermarry
std::invokeing &S::x returns by reference, while your lambda returns by value. I'm not sure why the former is illegal, but this seems to be the difference.Hawk
[] ( const S& l ) { return l.x; } returns a value, which is safe. And std::views::transform( &S::x ) uses a reference on a temporary, same as [] ( const S& l ) -> const float& { return l.x; }, which is UB: gcc.godbolt.org/z/9951nT486Whall
@Whall I still don't understand why it is UB when it is (float t:...) and not (const float& t:...).Cassondracassoulet
@WeijunZhou, temporary S destroyed before a reference on its field is accessed and copied in float t : Whall
I think the range loop creates a temporary gets, begin and destroys the temporary. This is the expectect behavior in C++ pre-26. changes have been made to the language to allow for this usage in C++26.Czerny
@Czerny Clang trunk with __cpp_range_based_for==202211L still have the issue, if that is what you're referring to.Cassondracassoulet
A
6

Your example is essentially equivalent to the example given in LWG 3502.

Since dereferencing views::transform(&S::f) yields a prvalue, views::transform(&S::x) will produce a reference into the materialized temporary that becomes dangling as soon as operator* returns, as described in the original issue.

Argil answered 17/6 at 7:43 Comment(0)
H
1

S::f() returns a temporary from its signature; S f() const;. Taking a reference to the member variable of this temporary is undefined behaviour.

Taking the value of the member variable of a temporary on the other hand is well defined behaviour which is why this is perfectly fine:

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

The lambda returns by value. Until C++23, Temporary range expression was UB.

The simple fix is to change the signature to return by lvalue reference; const S& f() const;. Everything else will then work correctly.

Hysell answered 17/6 at 6:17 Comment(11)
Why is taking const T& safer? Does it prolong the life of temporary here?Whall
Huh? Forming a pointer to a temporary would cause a compilation error, not UB. And OP is not doing that, they apply a member pointer to a temporary, which by itself is legal.Hawk
@Whall See Lifetime of temporaries explicitly bound to referencesHysell
@Hawk member pointer? I’m missing something aren’t I? C++ :)Hysell
&S::x is a member pointer. On the surface, OP's code seems to do this: S() .* &S::x, which by itself is legal. (But if you assign the result to a reference, it can dangle.)Hawk
You can change (const S& l) in the lambda to (S l), and the sanitizer won't complain.Cassondracassoulet
@Hawk And it will eventually be assigned to a reference because… (I’m not sure but my hunch is that it will because the result of std::views::transform is a view eventually.Hysell
Mhm, but this is not the explanation you give in your answer. :)Hawk
You show me my mistake my friend. Actually you taught me something new today. Thank you.Hysell
Until C++23, Temporary range expression was UB. Taking a reference to that is not okay. I’ll add that for completeness.Hysell
@Hawk again thank you. It will take quite some time but I promise I’ll get better at this language.Hysell

© 2022 - 2024 — McMap. All rights reserved.