A moved C++ container and MSVC++

Pages: 12
Jun 18, 2022 at 12:24am
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <vector>

int main()
{
   std::vector vec1 { 1, 2, 3, 4, 5 };

   std::cout << "vec1 size: " << vec1.size() << "\n\n";

   std::vector vec2 { std::move(vec1) };

   std::cout << "vec1 size: " << vec1.size() << '\n';
   std::cout << "vec2 size: " << vec2.size() << '\n';
}
vec1 size: 5

vec1 size: 0
vec2 size: 5

M'ok, the output of vec1 at line 8 is understandable. Shouldn't line 12 cause a thrown access violation since the vector no longer exists? Is the output at line 12 UB?

::confused::
Last edited on Jun 18, 2022 at 3:16pm
Jun 18, 2022 at 12:58am
Shouldn't line 12 cause an illegal access violation since the vector no longer exists?
The move constructor doesn't affect the lifetime of the moved-from object at all.

(As a reminder, the lifetime of a object with class type, like vec1, ends when its destructor begins. The move constructor doesn't call vec1's destructor.)

Is the output at line 12 UB?
By convention, a move assignment or move constructor leaves the moved-from object in an unspecified but valid state. Therefore you can do anything to a moved-from object as long as you don't assume anything about it.

For example, if line 12 was vec1.front(), that would be undefined behavior because front assumes the vector has at least one element. But vec1.size() is fine because size doesn't assume anything.

Finally, unspecified but valid does not always mean empty, but the standard library does actually empty out stuff that's moved from. This can occasionally make it easier to reuse objects:

1
2
3
4
5
6
7
8
9
10
std::vector<std::vector<int>> matrix;
std::vector<int> row_vector; 
for (int i = 0; i < 3; ++i)
{
  for (int j = 0; j < 10; ++j)
    row_vector.push_back(i*j);
  matrix.push_back(std::move(row_vector));

  // row_vector.clear() // redundant because row_vector is already empty here 
}
Last edited on Jun 18, 2022 at 4:51am
Jun 18, 2022 at 7:19am
the standard library does actually empty out stuff that's moved from

Be careful when relying on the state of moved-from objects that are not of move-only types because if the move falls back to copy (e.g. because the object you try to move from is accessed through const ref) the code will still compile and the object that you intended to move from will remain unaffected.

Also be careful with making assumptions based on particular implementations. It seems like the standard doesn't explicitly say that the state of a moved-from std::vector should be empty but some claim they can draw that conclusion based on other requirements (at least in any sane implementation).

https://stackoverflow.com/questions/17730689/is-a-moved-from-vector-always-empty
Last edited on Jun 18, 2022 at 7:24am
Jun 18, 2022 at 9:16am
Don't assume anything about a moved-from object other than it is a valid object of the type. Just because say std::vector behaves in a certain way in one compiler version doesn't mean it will work the same in another. It's likely it will - but don't assume this. Also you can't assume anything else about a non-standard container. Just because std::vector behaves in one way, doesn't mean that myvector also behaves this way. A moved-from object is taken to have a 'temporary' value which either isn't needed any more or is going to be overwritten. Don't use a std::move() if you use the object subsequently.
Jun 18, 2022 at 12:56pm
M'ok, the output of vec1 at line 8 is understandable. Shouldn't line 12 cause a thrown access violation since the vector no longer exists?

As others have pointed out, moving from an object does not destroy that object.

Anyway, even if you allocate an object on the heap (e.g. via new operator), then destroy that object (e.g. via delete operator) and finally try to access the "dangling" pointer, there is absolutely no guarantee that you'll get access violation. It's very possible that it just happens to "work", or that you'll get some "garbage" data...
Last edited on Jun 18, 2022 at 5:40pm
Jun 18, 2022 at 1:18pm
Well, at least Intellisense throws up a warning, eventually, of potential problems with that moved vector:
Warning	C26800	Use of a moved from object: ''vec1''

Now the programmer needs to examine why they are dinking around with a moved object.

Grey areas, I don't like 'em ;)
Jun 18, 2022 at 1:22pm
Reusing a vector that had its elements moved appears to be acceptable since the object is still valid:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>

int main()
{
   std::vector vec1 { 1, 2, 3, 4, 5 };

   std::cout << "vec1 size: " << vec1.size() << "\n\n";

   std::vector vec2 { std::move(vec1) };

   std::cout << "vec1 size: " << vec1.size() << '\n';
   std::cout << "vec2 size: " << vec2.size() << "\n\n";

   vec1 = { 5, 4, 3, 2, 1 };

   std::cout << "vec1 size: " << vec1.size() << '\n';
}
Jun 18, 2022 at 1:25pm
What grey area? move semantics are well known. Bottom line - don't use a moved-from object after the move.

There's even a whole book on the topic:

https://leanpub.com/cppmove


Jun 18, 2022 at 1:40pm
I'd call "unspecified but valid state" as being a grey area.
Jun 18, 2022 at 1:41pm
And I do own that eBook, I simply haven't gotten around to taking time to read it. I've been reading up on C++20.
Jun 18, 2022 at 2:54pm
The reason why I asked the question(s) is I was using the example from Rainer Grimm's "The C++ Standard" book (it was using std::set, not std::vector) in MSVC++. The full example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <set>
#include <utility>

int main(){

  std::cout << std::endl;
  
  std::set<int> set1{0, 1, 2, 3, 4, 5};
  std::set<int> set2{6, 7, 8, 9};
  for (auto s: set1) std::cout << s << " ";
  std::cout << " ----- ";
  for (auto s: set2) std::cout << s << " ";
  
  std::cout << std::endl;
  
  set1= set2;
  for (auto s: set1) std::cout << s << " ";
  std::cout << " ----- ";
  for (auto s: set2) std::cout << s << " ";
  
  std::cout << std::endl;
  
  set1= std::move(set2);
  for (auto s: set1) std::cout << s << " ";
  std::cout << " ----- ";
  for (auto s: set2) std::cout << s << " ";
  
  std::cout <<  std::endl;
  
  set2={60, 70, 80, 90};
  for (auto s: set1) std::cout << s << " ";
  std::cout << " ----- ";
  for (auto s: set2) std::cout << s << " ";
  
  std::cout << std::endl;
  
  std::swap(set1, set2);
  for (auto s: set1) std::cout << s << " ";
  std::cout << " ----- ";
  for (auto s: set2) std::cout << s << " ";

  std::cout << "\n\n";

}

When MSVC++ Intellisense whinged about the use, that made me take notice and question why the moved object was being used. Nothing in the accompanying text mentions using a moved object is a possible problem. I had thought using a moved object wasn't the best idea -- IOWs, don't use it in that state -- but then I'm not a C++ expert like Rainer Grimm is.

Admittedly the book isn't a "for beginner's" book, Herr Grimm states in the beginning the book is more of a reference.

Being self-taught I know there are gaps in my understanding of what is and isn't good practice. The gaps are probably quite large crevasses.
Last edited on Jun 18, 2022 at 3:14pm
Jun 18, 2022 at 4:18pm
Also be careful with making assumptions based on particular implementations. It seems like the standard doesn't explicitly say that the state of a moved-from std::vector should be empty [...]

I thought this was guaranteed by the standard, but it doesn't appear to be.
Now I have some defects to fix.

See https://wg21.link/n3241 which explains that the requirements for standard library types are stronger than the requirements for user-defined types passed into the standard library. The requirements for std types are intended as a "best practice", and produce the valid and unspecified state that we've been talking about.

In contrast the requirements on user-defined types passed to std are the barest minimum needed for the generic code to work. As seeplus points out, user-defined types might not be in a valid state after being moved. For instance a linked list's move constructor may move the sentinel node from the original list without allocating a replacement. This produces a state that's sometimes called "emptier than empty".

So in the program just above, line 27 might actually print stuff, but it never produces undefined behavior.

Thanks, Peter87 & seeplus!
Last edited on Jun 18, 2022 at 4:34pm
Jun 18, 2022 at 4:32pm
When MSVC++ Intellisense whinged about the use


VS is quite correct. It's warning about use of a moved object after the move which is basically a no-no. In this case it's for test purposes so OK here as just displaying the data.
Jun 18, 2022 at 4:36pm
@mbozzi, that latest snippet with MSVC++ does have some output from the moved object on line 27, nothing gets visibly displayed though.

Jun 18, 2022 at 5:18pm
@mbozzi, that latest snippet with MSVC++ does have some output from the moved object on line 27, nothing gets visibly displayed though.

Can't reproduce, the debugger shows that nothing is printed on line 27 (meaning operator<< never gets called).
I tried all 3 mainstream compilers and 3 standard libraries with the same results.

What am I getting wrong?

IIUC the move constructor could change the values of any elements that are left behind, so printing junk would still be compliant, if very unlikely.
Last edited on Jun 18, 2022 at 5:24pm
Jun 18, 2022 at 5:29pm
I'd say you are getting nothing wrong. The code as written using a moved object is technically legitimate apparently, though not good practice. :)

I get blank output from line 27, the surrounding output is visibly displayed, so what is actually happening I can't say. I never thought to use the debugger. :)

Essentially line 27 is treated as a null statement.
Jun 18, 2022 at 9:48pm
I think technically the only thing that a moved-from object needs to support is having its destructor called. But I think it's quite reasonable to at least be able to assign to an object that has been moved from. Lots of standard functions (e.g. std::swap) assumes it can do this.
Last edited on Jun 18, 2022 at 10:17pm
Jun 19, 2022 at 9:28am
I get blank output from line 27


That is what would be 'expected' (but not mandated). After L24, set2 could be empty as a valid state (almost certainly is for almost all implementations).


But I think it's quite reasonable to at least be able to assign to an object that has been moved from


As specified by the standard in that the moved-from object must be in a valid state after the move. Any valid non-const object that supports assignment can be assigned to.

Jun 19, 2022 at 9:36am
seeplus wrote:
As specified by the standard in that the moved-from object must be in a valid state after the move.

That's the default requirement that the standard places on standard library types if nothing else is mentioned.

https://eel.is/c++draft/lib.types.movedfrom

But as far as I know there is no such requirement on non-standard types.


seeplus wrote:
Any valid non-const object that supports assignment can be assigned to.

Does the standard require this? I'm not allowed to put preconditions on the assignment operator of my own class, saying that the object must not be in a moved-from state?


std::swap requires the type to fulfil the requirements of Cpp17MoveConstructible and Cpp17MoveAssignable.

https://eel.is/c++draft/utility.arg.requirements#tab:cpp17.moveconstructible

Cpp17MoveConstructible and Cpp17MoveAssignable say the state of the moved-from object rv is unspecified but they also have a note saying:
rv must still meet the requirements of the library component that is using it.
The operations listed in those requirements must work as specified whether rv has been moved from or not.

I guess that means that the requirements that std::swap puts on its arguments, that they should be move constructable and move assignable, must still be true after being moved-from. So it seems like it's more of a requirement of std::swap than a general requirement that we are all forced to follow everywhere (although it's probably a very good idea to do so).
Last edited on Jun 19, 2022 at 10:07am
Jun 19, 2022 at 10:34am
But as far as I know there is no such requirement on non-standard types.


Any 3rd party non-standard types that support move semantics that don't leave the object in a valid state are almost certainly going to get stamped upon from a very great height...
Pages: 12