C++11 rvalues and move semantics confusion (return statement)

前端 未结 6 1219
死守一世寂寞
死守一世寂寞 2020-11-22 04:29

I\'m trying to understand rvalue references and move semantics of C++11.

What is the difference between these examples, and which of them is going to do no vector cop

相关标签:
6条回答
  • 2020-11-22 04:52

    The simple answer is you should write code for rvalue references like you would regular references code, and you should treat them the same mentally 99% of the time. This includes all the old rules about returning references (i.e. never return a reference to a local variable).

    Unless you are writing a template container class that needs to take advantage of std::forward and be able to write a generic function that takes either lvalue or rvalue references, this is more or less true.

    One of the big advantages to the move constructor and move assignment is that if you define them, the compiler can use them in cases were the RVO (return value optimization) and NRVO (named return value optimization) fail to be invoked. This is pretty huge for returning expensive objects like containers & strings by value efficiently from methods.

    Now where things get interesting with rvalue references, is that you can also use them as arguments to normal functions. This allows you to write containers that have overloads for both const reference (const foo& other) and rvalue reference (foo&& other). Even if the argument is too unwieldy to pass with a mere constructor call it can still be done:

    std::vector vec;
    for(int x=0; x<10; ++x)
    {
        // automatically uses rvalue reference constructor if available
        // because MyCheapType is an unamed temporary variable
        vec.push_back(MyCheapType(0.f));
    }
    
    
    std::vector vec;
    for(int x=0; x<10; ++x)
    {
        MyExpensiveType temp(1.0, 3.0);
        temp.initSomeOtherFields(malloc(5000));
    
        // old way, passed via const reference, expensive copy
        vec.push_back(temp);
    
        // new way, passed via rvalue reference, cheap move
        // just don't use temp again,  not difficult in a loop like this though . . .
        vec.push_back(std::move(temp));
    }
    

    The STL containers have been updated to have move overloads for nearly anything (hash key and values, vector insertion, etc), and is where you will see them the most.

    You can also use them to normal functions, and if you only provide an rvalue reference argument you can force the caller to create the object and let the function do the move. This is more of an example than a really good use, but in my rendering library, I have assigned a string to all the loaded resources, so that it is easier to see what each object represents in the debugger. The interface is something like this:

    TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
    {
        std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
        tex->friendlyName = std::move(friendlyName);
        return tex;
    }
    

    It is a form of a 'leaky abstraction' but allows me to take advantage of the fact I had to create the string already most of the time, and avoid making yet another copying of it. This isn't exactly high-performance code but is a good example of the possibilities as people get the hang of this feature. This code actually requires that the variable either be a temporary to the call, or std::move invoked:

    // move from temporary
    TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));
    

    or

    // explicit move (not going to use the variable 'str' after the create call)
    string str("Checkerboard");
    TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));
    

    or

    // explicitly make a copy and pass the temporary of the copy down
    // since we need to use str again for some reason
    string str("Checkerboard");
    TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));
    

    but this won't compile!

    string str("Checkerboard");
    TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
    
    0 讨论(0)
  • 2020-11-22 04:57

    First example

    std::vector<int> return_vector(void)
    {
        std::vector<int> tmp {1,2,3,4,5};
        return tmp;
    }
    
    std::vector<int> &&rval_ref = return_vector();
    

    The first example returns a temporary which is caught by rval_ref. That temporary will have its life extended beyond the rval_ref definition and you can use it as if you had caught it by value. This is very similar to the following:

    const std::vector<int>& rval_ref = return_vector();
    

    except that in my rewrite you obviously can't use rval_ref in a non-const manner.

    Second example

    std::vector<int>&& return_vector(void)
    {
        std::vector<int> tmp {1,2,3,4,5};
        return std::move(tmp);
    }
    
    std::vector<int> &&rval_ref = return_vector();
    

    In the second example you have created a run time error. rval_ref now holds a reference to the destructed tmp inside the function. With any luck, this code would immediately crash.

    Third example

    std::vector<int> return_vector(void)
    {
        std::vector<int> tmp {1,2,3,4,5};
        return std::move(tmp);
    }
    
    std::vector<int> &&rval_ref = return_vector();
    

    Your third example is roughly equivalent to your first. The std::move on tmp is unnecessary and can actually be a performance pessimization as it will inhibit return value optimization.

    The best way to code what you're doing is:

    Best practice

    std::vector<int> return_vector(void)
    {
        std::vector<int> tmp {1,2,3,4,5};
        return tmp;
    }
    
    std::vector<int> rval_ref = return_vector();
    

    I.e. just as you would in C++03. tmp is implicitly treated as an rvalue in the return statement. It will either be returned via return-value-optimization (no copy, no move), or if the compiler decides it can not perform RVO, then it will use vector's move constructor to do the return. Only if RVO is not performed, and if the returned type did not have a move constructor would the copy constructor be used for the return.

    0 讨论(0)
  • 2020-11-22 05:00

    Not an answer per se, but a guideline. Most of the time there is not much sense in declaring local T&& variable (as you did with std::vector<int>&& rval_ref). You will still have to std::move() them to use in foo(T&&) type methods. There is also the problem that was already mentioned that when you try to return such rval_ref from function you will get the standard reference-to-destroyed-temporary-fiasco.

    Most of the time I would go with following pattern:

    // Declarations
    A a(B&&, C&&);
    B b();
    C c();
    
    auto ret = a(b(), c());
    

    You don't hold any refs to returned temporary objects, thus you avoid (inexperienced) programmer's error who wish to use a moved object.

    auto bRet = b();
    auto cRet = c();
    auto aRet = a(std::move(b), std::move(c));
    
    // Either these just fail (assert/exception), or you won't get 
    // your expected results due to their clean state.
    bRet.foo();
    cRet.bar();
    

    Obviously there are (although rather rare) cases where a function truly returns a T&& which is a reference to a non-temporary object that you can move into your object.

    Regarding RVO: these mechanisms generally work and compiler can nicely avoid copying, but in cases where the return path is not obvious (exceptions, if conditionals determining the named object you will return, and probably couple others) rrefs are your saviors (even if potentially more expensive).

    0 讨论(0)
  • 2020-11-22 05:01

    As already mentioned in comments to the first answer, the return std::move(...); construct can make a difference in cases other than returning of local variables. Here's a runnable example that documents what happens when you return a member object with and without std::move():

    #include <iostream>
    #include <utility>
    
    struct A {
      A() = default;
      A(const A&) { std::cout << "A copied\n"; }
      A(A&&) { std::cout << "A moved\n"; }
    };
    
    class B {
      A a;
     public:
      operator A() const & { std::cout << "B C-value: "; return a; }
      operator A() & { std::cout << "B L-value: "; return a; }
      operator A() && { std::cout << "B R-value: "; return a; }
    };
    
    class C {
      A a;
     public:
      operator A() const & { std::cout << "C C-value: "; return std::move(a); }
      operator A() & { std::cout << "C L-value: "; return std::move(a); }
      operator A() && { std::cout << "C R-value: "; return std::move(a); }
    };
    
    int main() {
      // Non-constant L-values
      B b;
      C c;
      A{b};    // B L-value: A copied
      A{c};    // C L-value: A moved
    
      // R-values
      A{B{}};  // B R-value: A copied
      A{C{}};  // C R-value: A moved
    
      // Constant L-values
      const B bc;
      const C cc;
      A{bc};   // B C-value: A copied
      A{cc};   // C C-value: A copied
    
      return 0;
    }
    

    Presumably, return std::move(some_member); only makes sense if you actually want to move the particular class member, e.g. in a case where class C represents short-lived adapter objects with the sole purpose of creating instances of struct A.

    Notice how struct A always gets copied out of class B, even when the class B object is an R-value. This is because the compiler has no way to tell that class B's instance of struct A won't be used any more. In class C, the compiler does have this information from std::move(), which is why struct A gets moved, unless the instance of class C is constant.

    0 讨论(0)
  • 2020-11-22 05:02

    None of those will do any extra copying. Even if RVO isn't used, the new standard says that move construction is preferred to copy when doing returns I believe.

    I do believe that your second example causes undefined behavior though because you're returning a reference to a local variable.

    0 讨论(0)
  • 2020-11-22 05:07

    None of them will copy, but the second will refer to a destroyed vector. Named rvalue references almost never exist in regular code. You write it just how you would have written a copy in C++03.

    std::vector<int> return_vector()
    {
        std::vector<int> tmp {1,2,3,4,5};
        return tmp;
    }
    
    std::vector<int> rval_ref = return_vector();
    

    Except now, the vector is moved. The user of a class doesn't deal with it's rvalue references in the vast majority of cases.

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