Last Update:
Moved or Not Moved - That Is the Question!
Table of Contents
C++11 brought Move Semantics. Since then we have extra capabilities to write faster code, support movable-only types, but also more headaches :). At least I have, especially when trying to understand the rules related to that concept. What’s more, we also have copy elision, which is a very common optimisation (and even mandatory in several cases in C++17). If you create an object based on another one (like a return value, or assignment), how do you know if that was copied or moved?
In this article I’ll show you two ways how to determine the status of a new object - copied, moved or copy-elision-ed. Let’s start!
Intro
Usually, when I try to show in my code samples that some object was moved or copied, I declared move operations for my type and then logged the message.
That worked, but how about built-in types? For example std::string
or std::vector
?
One day I was discussing a code sample related to std::optional
and JFT (a very experienced developer and very helpful!! See his articles here or here).
He showed me one trick that is simple but is very useful.
Let’s have a look at those two techniques now.
- Logging Move
That’s the most “explicit” way of showing if something was moved: add extra code to log inside move/copy constructors.
If you have a custom type and you want to see if the object was moved or not, then you can implement all the required move operations and log a message.
For a sample class, we have to implement all special member methods (the rule of five):
- copy constructor
- move constructor
- copy assignment operator
- move assignment operator
- destructor
class MyType {
public:
MyType(std::string str) : mName(std::move(str)) {
std::cout << "MyType::MyType " << mName << '\n';
}
~MyType() {
std::cout << "MyType::~MyType " << mName << '\n';
}
MyType(const MyType& other) : mName(other.mName) {
std::cout << "MyType::MyType(const MyType&) " << mName << '\n';
}
MyType(MyType&& other) noexcept : mName(std::move(other.mName)) {
std::cout << "MyType::MyType(MyType&&) " << mName << '\n';
}
MyType& operator=(const MyType& other) {
if (this != &other)
mName = other.mName;
std::cout << "MyType::operator=(const MyType&) " << mName << '\n';
return *this;
}
MyType& operator=(MyType&& other) noexcept {
if (this != &other)
mName = std::move(other.mName);
std::cout << "MyType::operator=(MyType&&) " << mName << '\n';
return *this;
}
private:
std::string mName;
};
(The above code uses a simple approach to implement all operations. It’s C++, and as usual, we have other possibilities, like the copy and swap idom).
Update: move and move assignment should be also marked with noexcept
. This improves exception safety guarantees and helps when you put your class in STL containers like vectors (see this comment: http://disq.us/p/23dfunz below the article). And also Core Guideline - C.66
When all of the methods are implemented, we can try using this type and checking the log output. Of course, if you have a more complicated class (more member variables), then you have to “inject” the logging code in the appropriate places.
One basic test:
MyType type("ABC");
auto tmoved = std::move(type);
The output:
MyType::MyType ABC
MyType::MyType(MyType&&) ABC
MyType::~MyType ABC
MyType::~MyType
Here, the compiler used move constructor. The content was stolen from the first object, and that’s why the destructor prints empty name.
How about move assignment?
The second test:
MyType tassigned("XYZ");
MyType temp("ABC");
tassigned = std::move(temp);
And the log message:
MyType::MyType XYZ
MyType::MyType ABC
MyType::operator=(MyType&&) ABC
MyType::~MyType
MyType::~MyType ABC
This time the compiler created two objects and then the content of XYZ
is overridden by ABC
.
Play with the code @Coliru.
Logging is relatively straightforward, but what’s the second option we could use?
- Looking at the Address
In the previous section, we worked with a custom type, our class. But what if you have types that cannot be modified? For example: the Standard Library types, like std::vector
or std::string
. Clearly, you shouldn’t add any logging code into those classes :)
A motivating code:
#include <iostream>
#include <string>
std::string BuildString(int number) {
std::string s { " Super Long Builder: " };
s += std::to_string(number);
return { s };
}
int main()
{
auto str42 = BuildString(42);
std::cout << str42;
}
In the above code, what happens to the returned value from BuildString()
? Is it copied, moved or maybe the copy is elided?
Of course, there are rules that specify this behaviour which are defined in the standard, but if we want to see it and have the evidence, we can add one trick.
What’s that?
Look at their .data()
property!
For example, you can add the following log statement:
std::cout << &s << ", data: " << static_cast<void *>(s.data()) << '\n';
To the BuildString
function and to main()
. With that we might get the following output:
0x7ffc86660010, data: 0x19fec40
0x7ffc866600a0, data: 0x19fec20
Super Long Builder: 42
The addresses of strings 0x7ffc86660010
and 0x7ffc866600a0
are different, so the compiler didn’t perform copy elision.
What’s more, the data pointers 0x19fec40
and 0x19fec20
are also different.
That means that the copy operation was made!
How about changing code from return { s };
into return s;
?
In that context we’ll get:
0x7ffd54532fd0, data: 0xa91c40
0x7ffd54532fd0, data: 0xa91c40
Super Long Builder: 42
Both pointers are the same! So it means that the compiler performed copy elision.
And one more test: return std::move(s);
:
0x7ffc0a9ec7a0, data: 0xd5cc50
0x7ffc0a9ec810, data: 0xd5cc50
This time the object was moved only. Such behaviour is worse than having full copy elision. Keep that in mind.
You can play with code sample @Coliru
A similar approach will work with std::vector
- you can also look at vector::data
property.
All in all:
- if the address of the whole container object is the same, then copy elision was done
- if the addresses of containers are different, but
.data()
pointers are the same, and then the move was performed.
One More Example
Here’s another example, this time the function returns optional<vector>
, and we can leverage the second technique and look at the address.
#include <iostream>
#include <string>
#include <vector>
#include <optional>
std::vector<int> CreateVec() {
std::vector<int> v { 0, 1, 2, 3, 4 };
std::cout << std::hex << v.data() << '\n';
//return {std::move(v)}; // this one will cause a copy
return (v); // this one moves
//return v; // this one moves as well
}
std::optional<std::vector<int>> CreateOptVec() {
std::vector<int> v { 0, 1, 2, 3, 4 };
std::cout << static_cast<void *>(v.data()) << '\n';
return {v}; // this one will cause a copy
//return v; // this one moves
}
int main() {
std::cout << "CreateVec:\n";
auto vec = CreateVec();
std::cout << static_cast<void *>(vec.data()) << '\n';
std::cout << "CreateOptVec:\n";
auto optVec = CreateOptVec();
std::cout << static_cast<void *>(optVec->data()) << '\n';
}
Play with code @Coliru
The example uses two functions that create and return a vector of integers and optional of vector of integers. Depending on the return statement, you’ll see different output. Sometimes the vector is fully moved, and then the data pointer is the same, sometimes the whole vector is elided.
Summary
This article is a rather straightforward attempt to show the “debugging” techniques you might use to determine the status of the object.
In one case you might want to inject logging code into all of the copy/move/assignment operations of a custom class. In the other case, when code injections are not possible, you can look at the addresses of their properties.
In the example section, we looked at the samples with std::optional
, std::vector
and also a custom type.
I believe that such checks might help in scenarios where you are not sure about the state of the object. There are rules to learn. Still, if you see proof that an object was moved or copied, it’s more comfortable. Such checks might allow you to optimise code, improve the correctness of it and reduce some unwanted temporary objects.
Some extra notes:
- Since we log into constructors and other essential methods, we might get a lot of data to parse. It might be even handy to write some log scanner that would detect some anomalies and reduce the output size.
- The first method - logging into custom classes - can be extended as a class can also expose
.data()
method. Then your custom class can be used in the context of the second debugging technique.
Once again, thanks to JFT for valuable feedback for this article!
Some references
- The View from Aristeia: The Drawbacks of Implementing Move Assignment in Terms of Swap
- Thomas Becker: C++ Rvalue References Explained
How about your code? Do you scan for move/copy operations and try to optimise it better? Maybe you found some other helpful technique?
I've prepared a valuable bonus if you're interested in Modern C++!
Learn all major features of recent C++ Standards!
Check it out here: