On the surface, option 2 seems like a good idea since it handles both lvalues and rvalues in a single function. However, as Herb Sutter notes in his CppCon 2014 talk Back to the Basics! Essentials of Modern C++ Style, this is a pessimization for the common case of lvalues.
If m_items
was "bigger" than items
, your original code will not allocate memory for the vector:
// Original code:
void someFunction(const std::vector<string>& items) {
// If m_items.capacity() >= items.capacity(),
// there is no allocation.
// Copying the strings may still require
// allocations
m_items = items;
}
The copy-assignment operator on std::vector
is smart enough to reuse the existing allocation. On the other hand, taking the parameter by value will always have to make another allocation:
// Option 2:
// When passing in an lvalue, we always need to allocate memory and copy over
void someFunction(std::vector<string> items) {
m_items = std::move(items);
}
To put it simply: copy construction and copy assignment do not necessarily have the same cost. It's not unlikely for copy assignment to be more efficient than copy construction — it is more efficient for std::vector
and std::string
†.
The easiest solution, as Herb notes, is to add an rvalue overload (basically your option 3):
// You can add `noexcept` here because there will be no allocation‡
void someFunction(std::vector<string>&& items) noexcept {
m_items = std::move(items);
}
Do note that the copy-assignment optimization only works when m_items
already exists, so taking parameters to constructors by value is totally fine - the allocation would have to be performed either way.
TL;DR: Choose to add option 3. That is, have one overload for lvalues and one for rvalues. Option 2 forces copy construction instead of copy assignment, which can be more expensive (and is for std::string
and std::vector
)
† If you want to see benchmarks showing that option 2 can be a pessimization, at this point in the talk, Herb shows some benchmarks
‡ We shouldn't have marked this as noexcept
if std::vector
's move-assignment operator wasn't noexcept
. Do consult the documentation if you are using a custom allocator.
As a rule of thumb, be aware that similar functions should only be marked noexcept
if the type's move-assignment is noexcept