C++: When to pass by value?
It is often recommended to always pass function and constructor parameters by const&
.
For “in” parameters, pass cheaply-copied types by value and others by reference to
const
.If the function is going to keep a copy of the argument, in addition to passing by
const&
, add an overload that passes the parameter by&&
and in the bodystd::move
it to its destination.
The issue with this recommendation is when the function/constructor takes multiple arguments who all need to be copied.
struct Copier {
Copier(const Debug& a, const Debug& b) : a(a), b(b) {}
Debug a, b;
};
What if we wanted to move the objects when calling the constructor, rather than copying every time? Following the usual
recommendation, we could create a &&
overload. This works if, when calling this constructor, we move both arguments,
as in Copier(std::move(a), std::move(b))
.
Copier(Debug&& a, Debug&& b) : a(std::move(a)), b(std::move(b)) {}
However, this will NOT work if we can only move one argument. If we call Copier(std::move(a), b))
, then the
const&
constructor overload will be selected, and BOTH values will be copied. We could write two more overloads, but
as you can imagine this quickly makes the API pretty messy as it explodes the necessary constructor overloads.
The solution here is to only have one constructor, taking all parameters than need to be copied by value. This will let the caller copy or move each argument separately.
Copier(Debug a, Debug b) : a(std::move(a)), b(std::move(b)) {}
Rule of Thumb
My personal rule of thumb: if the function/constructor will need to keep a copy of the object anyway, then pass by
value. This lets the caller make the copy or move the value. It also keeps the API clean with only one
function/constructor rather than many const&
and &&
overloads. The only drawback is that it causes an extra move
operation, which is almost always negligible.
Herb Sutter also discussed this issue in his CppCon 2014 talk, “Back to Basics! Modern C++ Style” (1:25:50).
Compiler Explorer
#include <iostream>
struct Debug {
Debug() = default;
Debug(const Debug&) {
std::cout << "Copied!" << std::endl;
}
Debug(Debug&&) {
std::cout << "Moved!" << std::endl;
}
};
struct Copier {
Copier(const Debug& a, const Debug& b) : a(a), b(b) {}
Copier(Debug&& a, Debug&& b) : a(std::move(a)), b(std::move(b)) {}
Debug a, b;
};
struct ByValueCopier {
ByValueCopier(Debug a, Debug b) : a(std::move(a)), b(std::move(b)) {}
Debug a, b;
};
int main() {
Debug a, b;
std::cout << "1" << std::endl;
Copier(a, b);
std::cout << "2" << std::endl;
Copier(std::move(a), b);
std::cout << "3" << std::endl;
Copier(std::move(a), std::move(b));
std::cout << std::endl;
std::cout << "4" << std::endl;
ByValueCopier(a, b);
std::cout << "5" << std::endl;
ByValueCopier(std::move(a), b);
std::cout << "6" << std::endl;
ByValueCopier(std::move(a), std::move(b));
}