Created
December 4, 2017 18:51
-
-
Save quantumelixir/94662eec025eb04842bc1bfcd4c8a324 to your computer and use it in GitHub Desktop.
Understand the semantics of moves vs. copies in C++
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Study to understand the semantics of moves vs. copies in C++. | |
// | |
// * These operations are different only for reference types like the | |
// String class defined below. Unlike value types, a literal bitwise | |
// copy does not make sense for reference types. Instead, a copy | |
// typically involves a heap allocation followed by a bitwise copy | |
// from the object(s) being pointed to. | |
// | |
// * The goal of a move operation is to avoid unnecessary copying (and | |
// the associated heap allocations) when possible. For instance, in | |
// the case of copying from a temporary it is more efficient to hijack | |
// the identity of the temporary instead of performing a (semantically | |
// correct) literal copy, which is the point of the move operation. | |
// | |
// * Today's compilers are so good at eliding unnecessary copies that | |
// sometimes you need to pass extra arguments to make sure the | |
// sequence of constructor calls follows what you wrote (and not just | |
// something that is equivalent). So compile with the following flags: | |
// $ g++ -Wall -std=c++14 -fno-elide-constructors copies-and-moves.cc | |
#include <cstring> | |
#include <iostream> | |
#include <string> | |
#include <vector> | |
template <class U> void say(U u) { std::cout << u << std::endl; } | |
template <class U, class... T> void say(U u, T... ts) { | |
std::cout << u; | |
say(ts...); | |
} | |
// Crazy string -- sequence of chars without a null terminator. | |
struct String { | |
String() : ptr_(nullptr), len_(0) { say("constructing an empty String"); } | |
// Construct from string literals. | |
String(const char *s) { | |
int n = 0; | |
while (s[n++] != '\0') | |
; | |
n--; // remember we don't want the null character! | |
ptr_ = new char[n]; // != nullptr even if n == 0. | |
len_ = n; | |
memcpy(ptr_, s, len_ / sizeof(char)); | |
say("[allocating] memory for a String §", this->ToString(), "§"); | |
} | |
// Destructor. | |
~String() { | |
if (!ptr_) | |
return; | |
say("[returning] memory for a String §", this->ToString(), "§"); | |
delete[] ptr_; | |
len_ = 0; | |
} | |
// Copy constructor. | |
String(const String &other) { | |
ptr_ = new char[other.len_]; | |
len_ = other.len_; | |
memcpy(ptr_, other.ptr_, other.len_ / sizeof(char)); | |
say("[allocating] copying a String §", this->ToString(), "§"); | |
} | |
// Assignment operator (return non-const reference because we don't | |
// want to disallow calling non-const methods on the returned | |
// String). Self assignment is fine because first we copy other to a | |
// temporary, then we free stuff owned by *this followed by a move | |
// to the temporary. | |
String &operator=(const String &other) { | |
say("call to copy-assignment on a String"); | |
return *this = String{other}; // copy to a temporary and move. | |
} | |
// Move-assignment. Self-assignment should be impossible here, no? | |
String &operator=(String &&other) { | |
say("[moving] call to move-assignment on String §", this->ToString(), | |
"§"); | |
this->~String(); | |
ptr_ = other.ptr_; | |
len_ = other.len_; | |
other.ptr_ = nullptr; | |
other.len_ = 0; | |
return *this; | |
} | |
// Move constructor | |
String(String &&other) { | |
say("[moving] to a String §", other.ToString(), "§"); | |
ptr_ = other.ptr_; | |
len_ = other.len_; | |
other.ptr_ = nullptr; | |
other.len_ = 0; | |
} | |
// For illustrating the method calls. | |
std::string ToString() const { return std::string(ptr_, len_); } | |
private: | |
char *ptr_; | |
int len_; | |
}; | |
std::ostream &operator<<(std::ostream &os, const String &s) { | |
return os << s.ToString(); | |
} | |
int main() { | |
String t; | |
{ | |
t = String{"lausanne"}; | |
t = t; // self assignment is not a problem. | |
std::cout << "---" << std::endl; | |
String x{String{"lucern"}}; // Move constructor is typically | |
// elided here, so you need to pass | |
// -fno-elide-constructors to see the | |
// move here. | |
std::cout << x << std::endl; | |
std::cout << "---" << std::endl; | |
String s{"zurich is the best!"}; | |
std::cout << s << std::endl; | |
t = s; | |
} | |
std::cout << "---" << std::endl; | |
std::cout << t << std::endl; | |
std::cout << "---" << std::endl; | |
String{"bern"} = String{"boring!"}; // Funny moves on a temporary. | |
return 0; | |
} |
Author
quantumelixir
commented
Dec 4, 2017
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment