3

I fail to understand the real nature std::move

  • They say that: std::move(object) returns "an Rvalue reference to that object". Yes, I got this.
  • They also say that: use push_back(std::move(object)) instead of push_back(object) to avoid copying. I didn't get this, because it seems to contradict with the example below:
#include <utility>      
#include <iostream>     
#include <vector>       
#include <string>       
#include <iomanip>
template <class T> void print(T const & object){
    for (auto var : object){std::cout << ' '<<var;}
    std::cout << '\n';
}
int main () {
  std::string foo = "foo-string";
  std::string bar = "bar-string";
  std::vector<std::string> myvector = { "super", "vip" , "pro" , "ok" }; // capcity=size=4;

  // this block is just to increase the capacity of the vector
  myvector.push_back( "rango" );      //NOW: capacity = 8 , size =5
  std::cout << &myvector[0] <<'\n';   //check the address of the first element of the vector

  // this block is the point of the problem
  myvector.push_back(foo);                  // copies -> YES!
  myvector.push_back(std::move(bar));       // moves !?!? 

  std::cout << "myvector contains:";
  print(myvector);

  std::cout << &myvector[0] << '\n';   // re-check the address of first element of the vector = still the same.
  std::cout << "the moved-from object: " << std::quoted(bar); 

  return 0;
}
  • The address of first element of vector before and after push_back(std::move(object)) is the same, meaning the address of the last element by push_back must be stored at "a fixed distance" from the first element (in this case)
  • So if we don't copy the object to the fixed given position, then how can we store the value of that object to "that position"?
chrysante
  • 2,328
  • 4
  • 24
Rango
  • 217
  • 8
  • 2
    There's also `emplace_back` if you prefer. – tadman Jul 19 '23 at 15:50
  • 4
    I don't understand why you think the address of the first element of the vector _would_ change. – Nathan Pierson Jul 19 '23 at 15:54
  • 2
    Recommendation: Replace `string` with a class that implements all of the special member functions so that you can log exactly what's going on, which function got called when, and then you can really start experimenting. – user4581301 Jul 19 '23 at 15:54
  • 2
    It is the content of `bar` which is important (moved element). but as string might implement SSO (Small String Optimization), use longer string as test (or other class). – Jarod42 Jul 19 '23 at 15:54
  • 5
    *"this block is just to increase the capacity of the vector"*. There is [`std::vector::reserve`](https://en.cppreference.com/w/cpp/container/vector/reserve). – Jarod42 Jul 19 '23 at 15:57
  • @NathanPierson , I have thought that we should move the vector to where the object is to void "COPYING" :D – Rango Jul 19 '23 at 16:02
  • 1
    The addresses in the vector won't change unless a re-allocation occurs! What's of interest for you is if the *data* contained within `bar` has been moved! So you might want to store `auto bar_data = bar.data()` before the move and compare it against `myvector[indexOfMovedBar].data()`. I'm rather sure this actually invokes undefined behaviour, but should still illustrate what actually goes on – provided you don't get victim of the short string optimisation mentioned already. – Aconcagua Jul 19 '23 at 16:05
  • 1
    `std::move` is just a cast, by itself it doesn't actually do anything, but it allows move constructors to be called and that's the important bit. – Jesper Juhl Jul 19 '23 at 16:10
  • 1
    [Illustration of my previous comment.](https://godbolt.org/z/9EM469rea) – Aconcagua Jul 19 '23 at 16:11
  • @Rango `std::move(bar)` means that `bar` may get moved from. I don't even know how you think it could cause `myvector` to "move" in the sense of changing its address. Note that I didn't even mention `myvector` until just now! – Nathan Pierson Jul 19 '23 at 16:47

4 Answers4

12

Let's break down what a move actually means in C++

  • For a trivial type it's the same as copying. The object will be copied byte by byte to its new location.
  • For a non-trivial type it can mean something else, depending on how the move constructor is implemented. A std::vector for example has a dynamically allocated buffer where it stores its elements. So when you move construct a std::vector from another std::vector the newly created object will take over (or steal if you will) the buffer from the old object. It will still be a new object though and have distinct memory location.

In the case of std::string things get a little more complicated. Conceptually std::string is very similar to std::vector<char> but it has what is called a small buffer optimization (in common implementations at least). This means, that if the string is under a certain length, it will be stored within the std::string object, not in a dynamically allocated buffer. Usually the threshold is around 20 characters. Only larger strings will be placed on the free store. Because your string "bar-string" is fairly short, it will be placed within the std::string object and thus a move results in a copy of the string.

chrysante
  • 2,328
  • 4
  • 24
4

When you do a push_back on the vector, a new element needs to be constructed to append to the vector. Your capacity is larger than the size, so there will be no reallocation - a new string is simply constructed at the end of the vector in the space that has already been allocated.

A string constructor will get called, but the question is which string constructor.

When you pass an lvalue reference, as in:

myvector.push_back (foo);

the new string at the end of the vector is copy-constructed from foo. When you pass an rvalue reference, however, as in:

myvector.push_back ( std::move(bar) );

you call the move constructor of the string. A string is essentially a pointer to dynamically-allocated memory (it's more than that, but that's the essential part of it). When you copy-construct a string, the string's contents need to be deep-copied. When you move-construct one, however, it's only a pointer copy.

The visible effect of calling the move constructor is that bar is invalidated by the move, but that enabled a shallow copy instead of a deep copy.

The layout of the vector will be exactly the same either way.

user1806566
  • 1,078
  • 6
  • 18
  • 2
    `bar` is not invalidated, but [left in valid, but unspecified state](https://en.cppreference.com/w/cpp/string/basic_string/basic_string)... Might be reset to the empty string or even contain the contents of the string moved to (or combination of both: use the target string's already allocated memory, thus get that one's capacity, but restart as empty string, i.e. `size` only reset to 0). – Aconcagua Jul 19 '23 at 16:16
3

Move construction reduces copying for objects which hold external resources.

For a std::string, the external resource is the character storage¹ (whose location you can find using its data() member).

When we move-construct the new string in the vector (myvector.push_back(std::move(bar))), that new string takes ownership of the character storage. That is the data that we avoided copying.

#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main()
{
    std::string foo = "foo-string extended - see footnote 1";
    std::vector<std::string>(v);
    std::cout << static_cast<void*>(foo.data()) << '\n';
    v.push_back(std::move(foo));
    std::cout << static_cast<void*>(v[0].data()) << '\n';
}

¹ For short strings, such as the ones in the question, your implementation may store the characters within the object itself. You might need longer strings to demonstrate the effect.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
1

So if we don't "COPY" the object to the FIXED given position, then how can we store the value of that object to "that position"?

There's the object, and then there's the data held by that object:

[string] -> [character buffer]

You understand that a std::string (barring Small String Optimization) on the stack points to data on the heap.

When you assign that string to another location, of course the string itself (very lightweight) is copied -- whether you use std::move or not.

The difference however, concerns the underlying data (character buffer):

  • copy: a new buffer has to be allocated, and all the data from the existing buffer has to be copied to it. That's the expensive part of the copy. Both strings are identical and modifiable independently.

    [old string] -------------> [character buffer 1]
    [new string] -------------> [character buffer 2]
    
  • move: the new string points to the old buffer, meaning we skip the expensive part. The old string has no more buffer -- it's left in an unspecified state (see EDIT below), and using it improperly will cause the worst errors.

    [old string] -> nothing!    [character buffer 1]
    [new string] ---------------^
    

EDIT: I originally wrote that moved-from strings get left in an invalid state -- which @Aconcagua pointed out to be wrong: the reference does state it's left in valid, but unspecified state. I had to refer to the following question to understand the difference: What constitutes a valid state for a "moved from" object in C++11?

hugo
  • 3,067
  • 2
  • 12
  • 22