Search code examples
c++gccclangc++20std-ranges

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


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?


Solution

  • 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.