Search code examples
c++c++11move-constructortemporary-objects

Can I move attributes of temporary objects in C++?


I'm writing an iterator that depends on items in a vector and an iterator-factory that spawns said iterators. The code is conceptually equal to the following:

struct Iter
{
  int i = 0;
  vector<int> vec;

  Iter(const vector<int>& _vec):
    vec(_vec)
  {
    cout << "copied vector of size "<<vec.size()<<" for iterator\n";
  }

  Iter(vector<int>&& _vec):
    vec(move(_vec))
  {
    cout << "moved vector of size "<<vec.size()<<" for iterator\n";
  }

  int& operator*() { return vec[i]; }
  Iter& operator++() { ++i; return *this; }
  bool operator!=(const Iter& _it) const { return false; }
};

struct Factory
{
  vector<int> fac_vec;

  Factory(const vector<int>& _v):
    fac_vec(_v)
  {
    cout << "copied vector of size " << fac_vec.size() << " for factory\n";
  }

  Factory(vector<int>&& _v):
    fac_vec(move(_v))
  {
    cout << "moved vector of size "<<fac_vec.size()<<" for factory\n";
  }

  Iter begin() { return Iter(fac_vec); }
  Iter end() { return Iter({}); }
};
int main(){
  for(const int i: Factory({1,2,3}))
    cout << i << "\n";
  return 0;
}

Now, running this code gives me (g++ 8.3):

moved vector of size 3 for factory   [ initialization of the factory with {1,2,3} - moved ]
copied vector of size 3 for iterator [ initialization of the begin-iterator with fac_vec - copied ]
moved vector of size 0 for iterator  [ initialization of the end-iterator with {} - moved ]

This is kind of disappointing as I was hoping that the last begin() would call Iter's move-constructor since the factory is being destroyed right after (isn't it?) and the compiler has all the information it needs to decide this.

I figure I could do what I want using std::shared_ptr but this incurs overhead both in the code and in the program. I'd much rather tell the compiler to move fac_vec when the last begin() is called. Is there a way to do this?


Solution

  • Usually, when implementing iterators, you take the vector's iterator or a reference to the vector:

    struct Iter
    {
      int i = 0;
      vector<int>* vec;
    
      Iter(vector<int>& _vec):
        vec(&_vec)
      {
        cout << "copied vector of size "<<vec->size()<<" for iterator\n";
      }
    
      int& operator*() { return (*vec)[i]; }
      Iter& operator++() { ++i; return *this; }
      bool operator!=(const Iter& _it) const { return false; }
    };
    

    That way you never copy.

    Better, use vector's own iterator:

    struct Iter
    {
      vector<int>::iterator it;
    
      Iter(vector<int>::iterator _it):
        it(_it)
      { }
    
      int& operator*() { return *it; }
      Iter& operator++() { ++it; return *this; }
      bool operator!=(const Iter& _it) const { return false; }
    };
    

    But then, you can drop Iter and simply use the vector iterators:

    struct Factory
    {
      // ...    
    
      auto begin() { return fac_vec.begin(); }
      auto end() { return fac_vec.end(); }
    };
    

    Now if you truely want to contain a value inside the iterator (not recommended).

    The compiler won't move fac_vec since it's a lvalue to a vector. No move here. You need an rvalue to a vector to move it.

    You can get that by overloading the function for rvalue reference instance:

    struct Factory
    {
      vector<int> fac_vec;
    
      Factory(const vector<int>& _v):
        fac_vec(_v)
      {
        cout << "copied vector of size " << fac_vec.size() << " for factory\n";
      }
    
      Factory(vector<int>&& _v):
        fac_vec(move(_v))
      {
        cout << "moved vector of size "<<fac_vec.size()<<" for factory\n";
      }
    
      // moving  when Factory is temporary
      Iter begin() && { return Iter(std::move(fac_vec)); }
      Iter end() && { return Iter({}); }
    
      // copying
      Iter begin() const& { return Iter(fac_vec); }
      Iter end() const& { return Iter({}); }
    };
    

    But the for loop won't call the move version. Why? It would be dangerous, since that would require calling move multiple times on a type.

    A range for is roughly equivalent to that:

    auto&& range = <range expr>; // forwarding reference to range (rvalue in your case)
    auto begin = range.begin(); // range is lvalue, so calls the const&
    auto end = range.end(); // range is lvalue, so calls the const&
    
    for (/* ... */) {
        // body
    }
    

    To use your move operations, it would require to cast the range to rvalue multiple times, potentially using a moved from value:

    auto begin = std::move(range).begin(); // range is lvalue but moved so calls the &&
    auto end = std::move(range).end(); // range is lvalue but moved so calls the &&
    

    If you want to use value in iterators which is not recommended, you cannot use range for loop and must use old style for loops.

    Keep in mind that calling like this move is oftentimes reguarded as code smell, and using move multiple time is even more so.