Are there any use cases for std::forward with a prvalue?

前端 未结 3 1136
醉梦人生
醉梦人生 2021-02-01 17:46

The most common usage of std::forward is to, well, perfect forward a forwarding (universal) reference, like

template
void f(T&         


        
相关标签:
3条回答
  • 2021-02-01 17:55

    I stared at this question before, read Howard Hinnant's link, couldn't fully grok it after an hour of thinking. Now I was looking and got the answer in five minutes. (Edit: got the answer is too generous, as Hinnant's link had the answer. I meant that I understood, and was able to explain it in a simpler way, which hopefully someone will find helpful).

    Basically, this allows you to be generic in certain kinds of situations depending on the typed that's passed in. Consider this code:

    #include <utility>
    #include <vector>
    #include <iostream>
    using namespace std;
    
    class GoodBye
    {
      double b;
     public:
      GoodBye( double&& a):b(std::move(a)){ std::cerr << "move"; }
      GoodBye( const double& a):b(a){ std::cerr << "copy"; }
    };
    
    struct Hello {
      double m_x;
    
      double & get()  { return m_x; }
    };
    
    int main()
    {
      Hello h;
      GoodBye a(std::forward<double>(std::move(h).get()));
      return 0;
    }
    

    This code prints "move". What's interesting is that if I remove the std::forward, it prints copy. This, for me, is hard to wrap my mind around, but let's accept it and move on. (Edit: I suppose this happens because get will return a lvalue reference to an rvalue. Such an entity decays into an lvalue, but std::forward will cast it into an rvalue, just as in the common use of forward. Still feels unintuitive though).

    Now, let's imagine another class:

    struct Hello2 {
      double m_x;
    
      double & get() & { return m_x; }
      double && get() && { return std::move(m_x); }
    };
    

    Suppose in the code in main, h was an instance of Hello2. Now, we no longer need std::forward, because the call to std::move(h).get() returns an rvalue. However, suppose the code is generic:

    template <class T>
    void func(T && h) {
      GoodBye a(std::forward<double>(std::forward<T>(h).get()));
    }
    

    Now when we call func, we'd like it to work properly with both Hello and Hello2, i.e. we'd like to trigger a move. That only happens for an rvalue of Hello if we include the outer std::forward, so we need it. But... We got to the punchline. When we pass an rvalue of Hello2 to this function, the rvalue overload of get() will already return an rvalue double, so std::forward is actually accepting an rvalue. So if it didn't, you wouldn't be able to write fully generic code as above.

    Damn.

    0 讨论(0)
  • 2021-02-01 18:03

    Ok since @vsoftco asked for concise use case here's a refined version (using his idea of having "my_forward" to actually see wich overload gets called).

    I interpret "use case" by providing a code sample that without prvalue not compile or behave differently (regardless of that would be really usefull or not).

    We have 2 overloads for std::forward

    #include <iostream>
    
    template <class T>
    inline T&& my_forward(typename std::remove_reference<T>::type& t) noexcept
    {
        std::cout<<"overload 1"<<std::endl;
        return static_cast<T&&>(t);
    }
    
    template <class T>
    inline T&& my_forward(typename std::remove_reference<T>::type&& t) noexcept
    {
        std::cout<<"overload 2"<<std::endl;
        static_assert(!std::is_lvalue_reference<T>::value,
                  "Can not forward an rvalue as an lvalue.");
        return static_cast<T&&>(t);
    }
    

    And we have 4 possible use cases

    Use case 1

    #include <vector>
    using namespace std;
    
    class Library
    {
        vector<int> b;
    public:
        // &&
        Library( vector<int>&& a):b(std::move(a)){
    
        }
    };
    
    int main() 
    {
        vector<int> v;
        v.push_back(1);
        Library a( my_forward<vector<int>>(v)); // &
        return 0;
    }
    

    Use case 2

    #include <vector>
    using namespace std;
    
    class Library
    {
        vector<int> b;
    public:
        // &&
        Library( vector<int>&& a):b(std::move(a)){
    
        }
    };
    
    int main() 
    {
        vector<int> v;
        v.push_back(1);
        Library a( my_forward<vector<int>>(std::move(v))); //&&
        return 0;
    }
    

    Use case 3

    #include <vector>
    using namespace std;
    
    class Library
    {
        vector<int> b;
    public:
        // &
        Library( vector<int> a):b(a){
    
        }
    };
    
    int main() 
    {
        vector<int> v;
        v.push_back(1);
        Library a( my_forward<vector<int>>(v)); // &
        return 0;
    }
    

    Use case 4

    #include <vector>
    using namespace std;
    
    class Library
    {
        vector<int> b;
    public:
        // &
        Library( vector<int> a):b(a){
    
        }
    };
    
    int main() 
    {
        vector<int> v;
        v.push_back(1);
        Library a( my_forward<vector<int>>(std::move(v))); //&&
        return 0;
    }
    

    Here's a resume

    1. Overload 1 is used, without it you get compilation error
    2. Overload 2 is used, without it you get compilation error
    3. Overload 1 is used, wihtout it you get compilation error
    4. Overload 2 is used, without it you get compilation error

    Note that if we do not use forward

    Library a( std::move(v));
    //and
    Library a( v);
    

    you get:

    1. Compilation error
    2. Compile
    3. Compile
    4. Compile

    As you see, if you use only one of the two forward overloads, you basically cause to not compile 2 out of 4 cases, while if you do not use forward at all you would get to compile only 3 out of 4 cases.

    0 讨论(0)
  • 2021-02-01 18:12

    This answer is for answering comment by @vsoftco

    @DarioOO thanks for the link. Can you maybe write a succinct answer? From your example it's still not clear for me why does std::forward need to be also defined for rvalues

    In short:

    Because without a rvalue specialization the following code would not compile

    #include <utility>
    #include <vector>
    using namespace std;
    
    class Library
    {
        vector<int> b;
    public:
        // hi! only rvalue here :)
        Library( vector<int>&& a):b(std::move(a)){
    
        }
    };
    
    int main() 
    {
        vector<int> v;
        v.push_back(1);
        A a( forward<vector<int>>(v));
        return 0;
    }
    

    however I can't resist to type more so here's also the not succint version of the answer.

    Long version:

    You need to move v because the class Library has no constructor accepting lvalue to it, but only a rvalue reference. Without perfect forwarding we would end up in a undesired behaviour:

    wrapping functions would incurr high performance penality when passing heavy objects.

    with move semantics we make sure that move constructor is used IF POSSIBLE. In the above example if we remove std::forward the code will not compile.

    So what is actually doing forward? moving the element without our consensus? Nope!

    It is just creating a copy of the vector and moving it. How can we be sure about that? Simply try to access the element.

    vector<int> v;
    v.push_back(1);
    A a( forward<vector<int>>(v)); //what happens here? make a copy and move
    std::cout<<v[0];     // OK! std::forward just "adapted" our vector
    

    if you instead move that element

    vector<int> v;
    v.push_back(1);
    A a( std::move(v)); //what happens here? just moved
    std::cout<<v[0];  // OUCH! out of bounds exception
    

    So that overload is needed to make possible a implicit conversion that is still safe, but not possible without the overload.

    Infact the following code will just not compile:

    vector<int> v;
    v.push_back(1);
    A a( v); //try to copy, but not find a lvalue constructor
    

    Real use case:

    You may argue that forwarding arguments may create useless copies and hence hide a possible performance hit, yes, that's actually true, but consider real use cases:

    template< typename Impl, typename... SmartPointers>
    static std::shared_ptr<void> 
        instancesFactoryFunction( priv::Context * ctx){
            return std::static_pointer_cast<void>( std::make_shared<Impl>(
    
                    std::forward< typename SmartPointers::pointerType>( 
                SmartPointers::resolve(ctx))... 
                )           );
    }
    

    Code was taken from my framework (line 80): Infectorpp 2

    In that case arguments are forwarded from a function call. SmartPointers::resolve's returned values are correctly moved regardless of the fact that constructor of Impl accept rvalue or lvalue (so no compile errors and those get moved anyway).

    Basically you can use std::foward in any case in wich you want to make code simpler and more readable but you have to keep in mind 2 points

    • extra compile time (not so much in reality)
    • may cause unwanted copies (when you do not explicitly move something into something that require a rvalue)

    If used with care is a powerfull tool.

    0 讨论(0)
提交回复
热议问题