C _Move_Reference - RicoJia/notes GitHub Wiki

========================================================================

Rvalue, Xvalue, Glvalue, PRvalue References

========================================================================

  1. Overview:
    1. glvalue (generalized l value), prvalue: pure-rvalue, like literals. Xvalue (将亡值). tutorial
      • glvalue = xvalue + lvalue, rvalue = prvalue + xvalue
    2. Expression Category: rvalue, lvalue. Value Category: r-value reference, etc, they are types.
      • expression can be: operatori and operand, constants, literals
      • reference is just an alias.

General Reference

  1. Reference are internally (also in binary) pretty much the same as pointer

    • If you have declared it only, even with value, that's still NOT definition. No memory is allocated.
  2. Reference to Bit Field

    struct Foo{
        std::uint32_t version:4,
                      IHL:3;
      };
    void f(std::size_t sz);
    Foo f;
    f(f.IHL);
    fwd(f.IHL);     //Error: fwd(const int& d) is a reference. A ref to bits cannot be created. So you need to create a copy first.
    • fix:
      auto i = static_cast<std::uint16_t>(f.IHL);
  3. reference collapsing: (C++98, 03). There are two types:

    • rvalue ref to rvalue -> rvalue: int&& &&
    • others, e.g, A& & -> A& (if A) (as long as there's lvalue reference, you will get lvalue reference)
    • Key mechanism:
      • explicit reference to reference is not allowed, but compiler can use it when deducing types
         int x = 3;
         auto& & rx = x;   // error
    • Refence Collapsing happens in 4 places:
      1. template
      2. auto
        auto&& k = x;     // if x could be int&, or int&&
      3. typedef or alias (using)
        template <typename T>
        class Widget{
            public: 
              typedef T&& Universal_Ref;    // T could be int&, or int&&
          }
      4.decltype

Rvalue Reference - c++11.

  1. Basic Concepts

    1. rvalue value cannot be assigned to an lvalue
      void foo(int&& T){++T; }
      viud bar(const int&& T){++T; }
      int i = 0; 
      foo(i);     //i becomes 1; 
      bar(i);     //error: this would bind an rvalue reference to an lvalue
    2. Scalar is never const rvalue
      7;    // This is int&&, not const int&&
    3. no object is of "lvalue" or "rvalue" (i.e,t no value-category), only "rvalue-reference".
      • The confusing point is: a named variable itself is always an lvalue. But it can have the type of rvalue-reference.
        // example 1
        Widget&& var1 = makeWidget(); //makeWidget returns an rvalue to var1, but var 1 itself is lvalue, since it's a named variable!
        // example 2
        template<typename T>
        class Widget {
          Widget(Widget&& rhs);        // rhs’s type is rvalue reference,
           ...                          // but rhs itself is an lvalue
        };
        • so to truly get transform these named variables to rvalue, we need perfect forwarding.
      • A rvalue reference can be indistinguishable from an lvalue reference: the diff is rvalue_reference can be bound to objects of rvalue category, and decltype() will be different
      • int&& rrx = 2: a temporary object of int is created, rvalue ref rrx is bound to the temp object. rrx is of lvalue in value category, since it has address.
        • we can modify the value of the temp object, through rrx = 4;
  2. Xvalue_reference

    1. both prvalue, xvalue can be rvalue
      • xvalue are: std::move(x), a[n] , a.m. 将亡值
      • prvalue are function return values.
    2. They are valid objects until ;. If they're not POD, you can treat them as:
      struct Foo{};
      Foo foo(){
          return Foo();
      }
      int main()
      {
          Foo f;
          foo() = f; // use default Foo& Foo(const Foo&), which returns an xvalue in this case. Treat it as a valid object until termination.
      }

unversal reference (official name: forwarding reference)

  1. Anything type that needs to be deduced, by template T or by auto. The foundation of universal reference, is type deduction in a reference-collapsing context.

    template <typename T>
    void f(T&& param);
    
    // Even const will make T&& an rvalue!
    template <typename T>
    void f(const T&& param);    
  2. This does not mean any template function T can have T&& as a universal reference. Distinguishing unversal reference and rvalue reference

    • R-value refs
      // 1. this is rvalue ref
      void foo(Widget&& param); 
      
      //2. Also r value - type deduction will not determine universal referenceness
      template <typename T>
      void f(std::vector<T>&& param){}  
    • The standard said: A forwarding reference is an rvalue reference to acv-unqualified template parameter, so const T&& is not a forwarding reference. link
      template <typename T>
      void const_r_value_ref(const T&& param){
          // const T&& param is actually const r-value reference
          // if param == 1, T is int, decltype(param) is int&&
          cout<<std::is_rvalue_reference<T>::value<<endl;
          cout<<std::is_rvalue_reference<decltype(param)>::value<<endl;
      }
      
      // Below won't compile:
      int i = 1;
      const_r_value_ref(i);
      • So one use is to disallow xvalue and prvalue: template <class T> void ref (const T&&) = delete;
  3. E.g, find the mistake in the above code

    #include <utility>
    void foo(int&&){}
    int main()
    {
        const int&& j = 7;        //const int&& is that rvalue?
        foo(std::forward<int>(j));
        return 0;
    }
    • we are passing in const int&& as int&& into foo(int&&), which is not allowed.
  4. Misc

    • const char(&a)[5] is an array of 5 char references
  5. reference qualifier for member class. This can be combined with function that returns rvalue.

    class Foo{
        public:
            Foo() = default;		//default constructor won't be generated automatically
            Foo(Foo&& ){cout<<"Foo move ctor"<<endl; }		//move constructor for constructing using a r-value 
    };
    
    class Bar{
        public: 
            Bar(){}
            // Does not go well with Bar().getFoo()
            Foo& getFoo()& {cout<<"lvalue foo is returned "<<endl; return foo;}	//foo is already lvalue
    
            // Will return Foo&, but later will trigger r_foo(const Foo&). So not efficient 
            // Also, cannot be overloaded with two getFoo()&& functions
            // Foo& getFoo()&& {
            //    cout<<"rvalue foo is returned "<<endl; 
            //    return foo;
            // }	
    
            // good
            //move is key
            Foo&& getFoo()&& {cout<<"rvalue foo is returned "<<endl; return std::move(foo);}	
        private:
            Foo foo; 
    }; 
    
    void rvalue_ref_func(){
        //r_foo is move-constructed
        auto r_foo = Bar().getFoo();
    }
    
    • if not marked with &&, a function can be accessed by both rvalue, or lvalue

========================================================================

Perfect Forwarding

========================================================================

  1. motivation: to allow the user to call a function, as if directly (which means preserving rvalue reference or lvalue reference)

    • Perfect forwarding is actually forwarding the reference, cv qualifiers.
    • The rvalue_reference object is an lvalue itself, so we need std::forward when passing in rvalue_reference
      using std::unique_ptr<int> = Ptr; 
      void(Ptr&&){
          Ptr q = std::make_unique<int>(4); 
          q = p;  // Error, need std::forward<int>
      }
    • **Use case: **
      template<typename T>
      void regular_r_ref(T&& param){
          cout<<std::is_rvalue_reference<decltype(param)>::value<<endl;
      }
      int main(){
          // constness
          int&& j = 8; 
          cout<<std::is_rvalue_reference<decltype(j)>::value<<endl;   // is rvalue ref
          regular_r_ref(std::forward<int>(j)); // is rvalue ref
          regular_r_ref(j); // NOT rvalue ref
      }
  2. Basics:

    • it's different from std::move, as move will simply cast a left value to right value.
    • std::forward will only cast, if the reference is initialized as rvalue. Therefore, we still need std::move for casting lvalue reference -> rvalue reference
  3. Cautions: perfect forwarding cannot be used on everything: Perfect forwarding is built on top of type deduction. If type deduction fails, perfect forwarding fails

    • Braced initializers:

      template <typename T>
      void f(T&& param){
        foo(std::forward<T> (param));
      }
      
      void foo(const std::vector<int>& v){
          cout<<"foo, vec"<<endl;
      }
      void type_deduction(){
              // Error: does not get type-deducted for templated functions.
              f({1});
      }
      • But {} gets deduced in auto. So you can use it as a fix
        auto il = {1,2,3};       //deduced to std::initializer_list
        f(il); 
    • 0 or NULL, will be deduced as int, not pointers.

      void foo(int* i); 
      f(0);      // 0 will be deduced as int. 
      f(nullptr)   // nullptr will be deduced as int*
    • Specific to std::string

      • char 16_t[] cannot be directly converted to std::string
      • If char 16_t[] is passed in as an argument and perfect-forwarded to std::string, error msg is ugly
  4. Simplified Implementation of std::forward

      //C++ 11
      template <typename T>
      T&& forward (typename std::remove_reference<T>::type& param){      //so param is lvalue reference
          return static_cast<T&&>(param);       // Reference collapsing: if T is int&, you will get int&, if int&&, you'll get int&&
      }
    
      //C++14
      template <typename T>
      T&& forward(std::remove_reference_t<T>& param){
          return static_cast<T&&>(param); 
        }

Move Semantics

  1. Short Summary

    • neither std::move or std::forward does anything in runtime.
    • std::move is casting to r-value reference UNCONDITIONALLY, like (Str&&)str.
      • Internally, std::move removes the reference and finds the type.
            template<typename T> // in namespace std
            typename remove_reference<T>::type&&		// 
            move(T&& param)
            {
             using ReturnType = // alias declaration;
             typename remove_reference<T>::type&&; // get the type of the input
             return static_cast<ReturnType>(param);}
        • T&& can be lvalue reference, as long as T is lvalue reference.
    • after std::move, lvalue cannot be retrieved, but can be assigned to and be used.
    • Another form: std::move( InputIt first, InputIt last, OutputIt d_first )
      • For std::vector, you can do vec2 = std::move(vec1)
      • But this works for general input, output itr
        std::move(queue_.begin(), queue_.end(), std::back_inserter(vec))        
  2. Motivation: When you want to construct an object from an r-value object, we need to allocate memory to the temp object, then copy it over.

    • we now want to directly "move" the r-value object into your object. This will be a performance boost.
    • The way we do that is by "stealing" the rvalue object address. Using Move Constructor. At the lowest level, move_constructor swaps addresses
      #include<utility>
      class Entity{
          public:
           Entity (Entity&& other){
                  if(&other != *this){
                          ...
                      }
               }
  3. std::move will respect constness, so if a const rvalue is passed in, you get const rvalue reference after.

    • Move Constructor does create r-value ref for const, because we might change the rvalue afterwards. So std::move doesn't gurantee if move_constructor will be called afterwards.
      const std::string str1 = "lol"; 
      std::string str2 = "hah"; 
      std::string s3(std::move(str1));			//calls the copy constructor cuz str1 is const 
      std::string s4(std::move(str2));			//calls move contructor of string. 

Cautions

  1. after std::move, the lvalue cannot be assigned again. So use this if you're sure that you won't need this lvalue anywhere else

  2. move might not be present, or it's not cheap, or the compiler chooses not to without noexcept or some lvalues cannot be moved:

    • First, not every class has move ctor
    • std::vector::push_back requires noexcept on move ctor. Otherwise, copy ctor is used.
    • move is cheap (constant time-complexity) because:
  3. In most containers, the content of the container is stored on heap, and the container stores a pointer to it.

    • Move in most containers is just transferring pointer ownership, then set the original ptr to null
    • std::array is not cheap. Because std::array is a C array with STL interface.
      • So std::array stores elements individually, there's not a single ptr.
      • Std::array will spend linear time "moving", same as copy
    • std::string under 15 chars will spend linear time moving, too.
      • Because std::string has "small string optmization" (SSO)
      • MOtivation is storing a small string on stack instead of on heap usuallty has a performance boost
      • However, "moving" a small string is linear time as copying.

std::move RVO (return value optimization, since C++98), NRVO

  1. Motivation: only return a reference when the object's lifetime is longer than the function. Builds the object directly in memory for return.

    • both are copy elision (meaning ignoring) part of "copy-elision". (An object is created and used directly, instead of being copied to where it's used.)
      1. native copy constructor vs copy ctro invoked by =.
      2. return value optimization (returning the local object from function directly)
        Foo makeFoo(){
          Foo f;
          return f;     // RVO takes place!
          //return std::move(f);
        }
  2. RVO is

    int Foo(){
        return 1; 
    }
  3. NRVO (Named Return Value Optimization): a named object is returned but not copied

    Bar foo(){
      Bar b;
      return b;
    }
  4. When NRVO doesn't happen:

    1. Deciding which object to return during runtime
        Bar foo(){
          Bar a, b;
          if (runtime_condition){
            return a;
          }
          else{
            return b;
            }
        }
    2. Return a global variable, or input Parameter
      Bar global_var;
      Bar foo_global(){return global_var; }
      Bar foo_param(Bar b){return b; }
    3. Return by std::move()
      Bar foo(){
        Bar b;
        return std::move(b);      // Disables NRVO
      }
    4. Assignment: using operator = with an existing object uses copy/move ctors, no RVO!
      Bar foo(){
          return Bar();
      }
      int main(){
          Bar s;
          s = foo();    // s already exists, no RVO performed
      }
    5. Return a member
      struct Bar{
          Widget w;
      }
      Widget woo(){return Bar().w; }

Misc

  1. std::ref

    • Creates a reference_wrapper object that acts like a reference.
    • Uses
      1. in std::thread, cuz it copies arguments into func cpp int i = 3; void func(int&); std::thread(func, std::ref(i));
      2. in a container. cpp int& arr[] = {x,y,z}; //illegal //use reference wrapper instead reference_wrapper<int> arr[] {x,y,z};
  2. std::reference_wrapper makes a struct copyable, assignable references. You can directly use it as a reference.

    • **Use Case: ** 可以用 predicate 和 reference wrap 来达到简化memory use 和code length:
      #include <functional>
      std::vector<std::reference_wrapper<type>> vec(type_vec.begin(), type_vec.end());
      vec.front().get().data_member       //when you use member accessor on a reference wrapper, it s operating on the wrapper, not the object being wrapped. So you need get().
    • since c++ 17, the iterator is the same as S&. otherwise, you have to manually unwrap the reference wrapper
  3. std::ref vs std::reference_wrapper

    • std::reference_wrapper wraps around a ptr, copyable and movable. Not working with rvalue references.
    • std::ref returns an std::reference_wrapper object
    • template <typename T> will usually be by-value, except this case
      foo<int&>(f);
⚠️ **GitHub.com Fallback** ⚠️