copies and stuff
this is the guide on keeping track of copies in a member function that’s Ingesting something. you need to have at least one copy or move so that your caller isn’t beholden to make sure that what it gave you stays in scope forever. but it’s easy to accidentally do a lot more than one copy or move.
the instrumentation
#include <fmt/core.h>
class CopyCounter {
public:
CopyCounter(int id) : counterId(id) {}
// Copy constructor
CopyCounter(const CopyCounter& other) {
counterId = other.counterId;
++copyCount;
fmt::println("+copy id {} (construct)", counterId);
}
// Move constructor
CopyCounter(CopyCounter&& other) noexcept {
counterId = other.counterId;
++moveCount;
fmt::println("+move id {} (construct)", counterId);
}
// Assignment operator
CopyCounter& operator=(const CopyCounter& other) {
if (this != &other) {
counterId = other.counterId;
++copyCount;
fmt::println("+copy id {} (assign)", counterId);
}
return *this;
}
// Move assignment operator
CopyCounter& operator=(CopyCounter&& other) noexcept {
if (this != &other) {
counterId = other.counterId;
++moveCount;
fmt::println("+move id {} (assign)", counterId);
}
return *this;
}
static void ResetCounts(std::string title = "") {
copyCount = 0;
moveCount = 0;
auto sidepad = (80 - title.length()) / 2;
fmt::println(std::string(sidepad, '-') + title + std::string(sidepad, '-'));
}
static void PrintCounts() {
fmt::println("total copies {}, total moves {}", copyCount, moveCount);
}
static int GetCopyCount() { return copyCount; }
static int GetMoveCount() { return moveCount; }
int counterId;
private:
static int copyCount;
static int moveCount;
};
// Initialize static class members
int CopyCounter::copyCount = 0;
int CopyCounter::moveCount = 0;
this is a static class that counts the number of copies and moves. Note the initializing of static members which has to go outside of the class definition.
the toy
template <class T>
class LinkedList {
public:
struct LinkedListNode {
T value;
LinkedListNode(T val) : value(val) {};
};
LinkedListNode* head = nullptr;
void PushFront(T value) {
auto newNode = new LinkedListNode(value);
head = newNode;
// Rest of the logic would go here but we don't really care
}
};
this is a toy linked-list-like class that ingests a T and needs to store it in a LinkedListNode. we want to get the number of copies down to the absolute minimum. we also want to be good class authors and respect the principle of least surprise, which means that:
- passing in an lvalue should make a copy
- passing in an lvalue with std::move should move
- passing in an rvalue should move
for the first example, we code PushFront the most naive way, taking value by copy, and constructing LinkedListNode by copy.
first example (all pass by copy)
the driver code
int main() {
CopyCounter::ResetCounts(
"pushfront by copy, construct listnode by copy");
auto c = CopyCounter(0);
// Push lvalue
list.PushFront(c);
c.counterId = 1;
// Push lvalue wrapped in std::move
list.PushFront(std::move(c));
// Push rvalue
list.PushFront(CopyCounter(2));
CopyCounter::PrintCounts();
return 0;
}
returns:
+copy id 0 (construct)
+copy id 0 (construct)
+copy id 0 (construct)
+move id 1 (construct)
+copy id 1 (construct)
+copy id 1 (construct)
+copy id 2 (construct)
+copy id 2 (construct)
total copies 7, total moves 1
Where we can see passing an lvalue required 3 copies, moving an lvalue got 1 move and 2 copies, and passing an rvalue required 2 copies. For the lvalue that’s:
- copied from
c
to function parametervalue
- copied from function parameter
value
to constructor parameterval
- copied from
val
to member variablevalue
For lvalue move, the counter object moved to the function parameter value
and the rest is the same, and for the rvalue it’s constructed directly in value
and the rest is the same.
try to take pass by reference
if we try to change the signature of PushFront so we can take pass by reference, be careful to mark it const, since temporaries can only bind to const lvalue references (don’t go trying to modify a temporary!!). below is with clang-19.
void PushFront(T& value) {
auto newNode = new LinkedListNode(value);
head = newNode;
// Rest of the logic would go here but we don't really care
}
// <source>:101:20: error: non-const lvalue reference to type 'CopyCounter' cannot bind to a temporary of type 'CopyCounter'
// 101 | list.PushFront(CopyCounter(1));
with a const reference it works:
void PushFront(const T& value) {
auto newNode = new LinkedListNode(value);
head = newNode;
// Rest of the logic would go here but we don't really care
}
and we get (same driver code)
+copy id 0 (construct)
+copy id 0 (construct)
+copy id 1 (construct)
+copy id 1 (construct)
+copy id 2 (construct)
+copy id 2 (construct)
total copies 6, total moves 0
So we remove the copy to the function parameter. One down yay! But we can do better.
how to do forwarding references
out of curiosity let’s try forwarding (universal) references. this is my first naive try:
void PushFront(T&& value) {
auto newNode = new LinkedListNode(value);
head = newNode;
// Rest of the logic would go here but we don't really care
}
// <source>:100:20: error: rvalue reference to type 'CopyCounter' cannot bind to lvalue of type 'CopyCounter'
// 100 | list.PushFront(c);
oop, that won’t take an lvalue. and adding const doesn’t help either. there’s two ways out. first, we can try naively adding an overload for an lvalue reference. but there’s a faustian bargain.
void PushFront(T&& value) {
auto newNode = new LinkedListNode(std::forward<T>(value));
head = newNode;
// Rest of the logic would go here but we don't really care
}
void PushFront(const T& value) {
PushFront((T)value);
}
if you try to implement one of them via the other (to not have to have the same function body twice), this gets us (same driver code)
---pushfront by const lvalue or rvalue reference, construct listnode by copy---
+copy id 0 (construct)
+move id 0 (construct)
+copy id 0 (construct)
+move id 1 (construct)
+copy id 1 (construct)
+move id 2 (construct)
+copy id 2 (construct)
total copies 4, total moves 3
which is bad because we have to do a copy just to move it again. which is pointless. same goes for the other way around, if you turn the rvalue into an lvalue before passing it to the other one.
what you have to do instead is this:
template <class U>
void PushFront(U&& value) {
auto newNode = new LinkedListNode(std::forward<U>(value));
head = newNode;
// Rest of the logic would go here but we don't really care
}
which results in:
---------pushfront by forwarding reference, construct listnode by copy---------
+copy id 0 (construct)
+copy id 0 (construct)
+move id 1 (construct)
+copy id 1 (construct)
+move id 2 (construct)
+copy id 2 (construct)
total copies 4, total moves 2
be careful though. you need to std::forward<U> not std::forward<T>, otherwise we break our principle for lvalue arguments:
auto newNode = new LinkedListNode(std::forward<T>(value));
// +move id 0 (construct) <-- oops, lvalue argument did a move!
// +copy id 0 (construct)
// +move id 1 (construct)
// +copy id 1 (construct)
// +move id 2 (construct)
// +copy id 2 (construct)
// total copies 3, total moves 3
auto newNode = new LinkedListNode(std::forward<U>(value));
// +copy id 0 (construct) <-- that's better
// +copy id 0 (construct)
// +move id 1 (construct)
// +copy id 1 (construct)
// +move id 2 (construct)
// +copy id 2 (construct)
// total copies 4, total moves 2
The reason why the copy turns into a move if you use T is left as an exercise to the intrepid reader.
constructing LinkedListNode by reference
okay let’s finally remove the last copy happening in the constructor of LinkedListNode. we’ll take the same approach as the function, using a templated constructor so we don’t have extra copies. although we could implement just the rvalue constructor and use it internally in our class only with std::forward, if we ever construct by lvalue we’ll get copies. might as well template it and cover both cases.
// We can just have this constructor and it's fine
explicit LinkedListNode(T&& val) : value(std::forward<T>(val)) {};
// But if we ever need to use this it's a copy
explicit LinkedListNode(const T& val) : value(val) {};
so instead we implement
struct LinkedListNode {
T value;
template <class U>
explicit LinkedListNode(U&& val) : value(std::forward<U>(val)) {};
};
which gets us the ideal result:
-pushfront by forwarding reference, construct listnode by forwarding reference-
+copy id 0 (construct)
+move id 1 (construct)
+move id 2 (construct)
total copies 1, total moves 2
which we retain even if in the future LinkedListNodes are constructed by lvalues. we maintain our principles of good API design and minimize copies. Yay!!