Skip to content

Instantly share code, notes, and snippets.

@quantumelixir
Created December 4, 2017 18:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save quantumelixir/94662eec025eb04842bc1bfcd4c8a324 to your computer and use it in GitHub Desktop.
Save quantumelixir/94662eec025eb04842bc1bfcd4c8a324 to your computer and use it in GitHub Desktop.
Understand the semantics of moves vs. copies in C++
// 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;
}
@quantumelixir
Copy link
Author

quantumelixir commented Dec 4, 2017

constructing an empty String
[allocating] memory for a String §lausanne§
[moving]     call to move-assignment on String §§
call to copy-assignment on a String
[allocating] copying a String §lausanne§
[moving]     call to move-assignment on String §lausanne§
[returning]  memory for a String §lausanne§
---
[allocating] memory for a String §lucern§
[moving]     to a String §lucern§
lucern
---
[allocating] memory for a String §zurich is the best!§
zurich is the best!
call to copy-assignment on a String
[allocating] copying a String §zurich is the best!§
[moving]     call to move-assignment on String §lausanne§
[returning]  memory for a String §lausanne§
[returning]  memory for a String §zurich is the best!§
[returning]  memory for a String §lucern§
---
zurich is the best!
---
[allocating] memory for a String §boring!§
[allocating] memory for a String §bern§
[moving]     call to move-assignment on String §bern§
[returning]  memory for a String §bern§
[returning]  memory for a String §boring!§
[returning]  memory for a String §zurich is the best!§

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment