Why std::make_move_iterator works on vector<string> but not on vector<int>
Asked Answered
M

3

9

I was expecting that std::make_move_iterator will always move contents, but it seems not.

It looks like it is moving elements in vector<string> but not in vector<int>.

See the below code snippet:

#include <iostream>
#include <iterator>
#include <string>
#include <vector>

void moveIntVector()
{
  std::cout << __func__ << std::endl;
  std::vector<int> v1;
  for (unsigned i = 0; i < 10; ++i) {
    v1.push_back(i);
  }
  std::vector<int> v2(
    std::make_move_iterator(v1.begin() + 5),
    std::make_move_iterator(v1.end()));
  std::cout << "v1 is: ";
  for (auto i : v1) {
    std::cout << i << " ";
  }
  std::cout << std::endl;

  std::cout << "v2 is: ";
  for (auto i : v2) {
    std::cout << i << " ";
  }
  std::cout << std::endl;
}

void moveStringVector()
{
  std::cout << __func__ << std::endl;
  std::vector<std::string> v1;
  for (unsigned i = 0; i < 10; ++i) {
    v1.push_back(std::to_string(i));
  }
  std::vector<std::string> v2(
    std::make_move_iterator(v1.begin() + 5),
    std::make_move_iterator(v1.end()));
  std::cout << "v1 is: ";
  for (auto i : v1) {
    std::cout << i << " ";
  }
  std::cout << std::endl;

  std::cout << "v2 is: ";
  for (auto i : v2) {
    std::cout << i << " ";
  }
  std::cout << std::endl;
}

int main()
{
  moveIntVector();
  moveStringVector();
  return 0;
}

The result is:

moveIntVector
v1 is: 0 1 2 3 4 5 6 7 8 9  # I expect this should be `0 1 2 3 4` as well!
v2 is: 5 6 7 8 9 
moveStringVector
v1 is: 0 1 2 3 4      
v2 is: 5 6 7 8 9 

I'm on Ubuntu 14.04, gcc 4.8.2 and the code is compiled with -std=c++11

Could you explain why std::make_move_iterator have different behaviour on vector<int> and vector<string>? (Or is it a bug?)

Mirthamirthful answered 29/7, 2014 at 6:27 Comment(2)
Note that in your moveStringVector's output for v1, there are extra spaces at the end from the empty strings left over from the move.Tiger
Err... did you seriously create two functions move*Vector() which handle a template class for two types in exactly identical fashion and are 99% identical to the letter? Whose name even indicates that the type is the only difference? It's bothering me so much that I feel this urge to go right now and consolidate that into a template function ;-). I mean, even the output operator is a template (as opposed to printf). The only difference would be in the initialization which could be passed in as an array. Or, actually, initialize the vector with an initializer list.Saltus
V
25

The behaviour is expected. A move from both vectors leaves the original v1 with 5 moved-from elements in their second half.

The difference is that when the strings are moved, what is left behind is empty strings. This is because it is a very efficient way to move strings, and leave the moved-from string in a self-consistent state (Technically, they could be left to hold the value "Hello, World, nice move!", but that would incur extra cost). The bottom line is that you don't see those moved-from strings in your output.

In the case of the int vectors, there is no way to move an int that is more efficient than copying it, so they are just copied over.

If you check the sizes of the vectors, you will see the v1 have size 10 in both cases.

Here's a simplified example to illustrate that the moved from strings are left empty:

#include <iostream>
#include <iterator>
#include <string>
#include <vector>

int main() 
{
    std::vector<std::string> v1{"a", "b", "c", "d", "e"};
    std::vector<std::string> v2(std::make_move_iterator(v1.begin()),
                                std::make_move_iterator(v1.end()));

    std::cout << "v1 size " << v1.size() << '\n';
    std::cout << "v1: ";
    for (const auto& s : v1) std::cout << s << " - ";
    std::cout << '\n';

    std::cout << "v2 size " << v2.size() << '\n';
    std::cout << "v2: ";
    for (const auto& s : v2) std::cout << s << " - ";
    std::cout << '\n';
}

Output:

v1 size 5
v1:  -  -  -  -  - 
v2 size 5
v2: a - b - c - d - e - 
Volitive answered 29/7, 2014 at 6:35 Comment(6)
+1, but I'd put it differently: there are efficient ways to move an int, and the most efficient way is to copy it. (I define "move" from a to b as making sure b gets the value of a, and a remains in a valid state. Copying is always one way of moving, even for string, it just so happens that it's not a particularly good way for string.)Senna
@hvd I agree. I re-phrased that part of the answer.Volitive
Is there any way to really move (or split) the vector into two parts? e.g. in my case, I would like to get v1 with first 5 elements and v2 with last 5 elements, while not copying them but actually moving (splitting) them.Mirthamirthful
@Mirthamirthful The simplest way that springs to mind is to erase the unwanted elements after moving them: v0.erase(v0 + 5, v0.end());Volitive
OK. Got it though it is not what I want. Anyway, that explains why it's better to operate with iterators rather than the containers.Mirthamirthful
@Mirthamirthful You can move the entire vector, typically leaving an empty vector behind. Interesting idea to move something like a slice into another container. For a vector that would only be efficient for end slices, but for other containers it wouldn't matter.Saltus
U
8

When we talk about a move we are not talking about moving the object itself (it remains intact). What gets moved are its internal data. This may or may not affect the value of the object whose internal data gets moved.

That is why your int array doesn't loose its original ints. As to your string example, it still has the original std::strings just like the int example but their internal values have changed to empty strings.

It is important to remember that internally a std::string (essentially) holds a pointer to a character array. So when you copy a std::string you copy every element of the character array. A move, however, avoids doing all that copying by copying the internal pointer instead.

But if the move operation stopped there that would leave both std::strings pointing at the same character array and changing the character data pointed to by either std::string would also change the other's. So when you move a string it is not enough to merely copy the internal pointer, you have to make the internal pointer of the std::string you moved from point to a new blank character array so that it can no longer affect the string its data was moved to.

When moving an int there is no further action required after the copy of its data. There are no pointers involved so after the copy both ints contain independent data.

Uriia answered 29/7, 2014 at 7:37 Comment(0)
S
0

move constructor is like of an object works like taking a regular reference and a instruction to move things. the default move constructor tries to call the move constructor of all member variables. a user defined one... pretty much it's up to the programmer to tell it what to do. you could program your objects to be in a undefined state after being subject to a move constructor, you can keep them unchanged(the destructor will still be called so you need to take care of that), you can keep them valid. strings will have a defined state after being subject to a move constructor.

as for your example...

int is trivially copyable and it's move constructor won't do anything but copying.

string is not trivially copyable. it has some dynamic stuff in it that the move constructor moves. and the previous one is left with a length of zero, you ARE printing them, along with the trailing "space" which you added. it's just they are the last 5 elements, at the end of what your printing and you aren't noticing it because it's equivalent to 5 trailing white spaces.

Strangles answered 19/1, 2021 at 9:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.