Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Returning Vectors Efficiently
Return vectors from functions efficiently using move semantics.
Returning std::vector and an introduction to move semantics
When passing std::vector to functions, we use const reference to avoid expensive copies. Surprisingly, returning std::vector by value is perfectly fine and often preferred.
Wait... what?
Copy semantics: when copies are necessary
Consider this straightforward example:
#include <iostream>
#include <vector>
int main()
{
std::vector scores{ 85, 92, 78, 90 };
std::vector backup{ scores }; // Copies scores into backup
scores[0] = 100; // Modify scores
backup[0] = 50; // Modify backup
std::cout << scores[0] << ' ' << backup[0] << '\n';
return 0;
}
This prints 100 50. When initializing backup from scores, the copy constructor creates an independent copy. Both vectors exist separately, and modifying one doesn't affect the other.
Copy semantics refers to the rules governing how objects are copied. When an object is copied, each data member is typically copied as well. For std::vector, this means allocating new memory and copying all elements.
When copying isn't optimal
Now consider this variant:
#include <iostream>
#include <vector>
std::vector<int> generateLevels()
{
std::vector levels{ 1, 5, 10, 15, 20 };
return levels;
}
int main()
{
std::vector playerLevels{ generateLevels() };
std::cout << playerLevels[0] << '\n';
return 0;
}
When playerLevels is initialized with the return value from generateLevels(), that return value is a temporary object. This temporary will be destroyed immediately after initialization completes. We need the data from the temporary, but the temporary itself becomes useless.
Traditionally, copy semantics would make an expensive copy of all the data, then destroy the original temporary (and its data). That's wasteful.
Introducing move semantics
What if instead of copying, we could simply transfer ownership of the data? The new vector would take over the data, and the temporary would be left empty (which is fine, since it's about to be destroyed anyway). Transferring ownership is typically just a few pointer assignments—vastly cheaper than copying!
This is the essence of move semantics: transferring data ownership from one object to another rather than copying it. When move semantics is invoked, data members that can be moved are moved, and those that can't are copied. For types with expensive-to-copy data (like std::vector and std::string), moving provides dramatic performance gains.
Core Understanding: Move semantics is an optimization allowing efficient data transfer between objects without expensive copying. Movable data is transferred; unmovable data is copied.
How move semantics activates
Move semantics kicks in automatically when these conditions are met:
- The object's type supports move semantics
- The initializer or assignment source is an rvalue (temporary) of the same type
- The move operation isn't optimized away (elided)
Fortunately, both std::vector and std::string support move semantics!
Returning move-capable types by value
Since functions return rvalues, and rvalues trigger move semantics, we can safely return move-capable types by value:
#include <iostream>
#include <vector>
std::vector<double> calculateRatios()
{
std::vector<double> ratios{ 0.25, 0.50, 0.75, 1.0 };
return ratios; // Moves (not copies) the data
}
int main()
{
std::vector<double> multipliers{ calculateRatios() };
std::cout << multipliers[2] << '\n';
return 0;
}
The returned vector is moved into multipliers rather than copied, making the operation inexpensive despite returning by value.
Core Understanding: Move-capable types like std::vector and std::string should be returned by value. They'll efficiently move their data rather than making expensive copies. These types should still be passed by const reference.
Why pass by reference but return by value?
This seems contradictory at first. Let's examine the four steps of passing data to and from functions:
- Construct the argument value
- Pass the value to the function
- Construct the return value
- Return the value to the caller
Here's an example showing all four steps:
#include <iostream>
#include <vector>
std::vector<int> doubleFirst(std::vector<int> input)
{
std::vector result{ input[0] * 2 };
return result;
}
int main()
{
std::vector values{ 42 }; // 1. Construct argument
std::cout << doubleFirst(values)[0] << '\n'; // 2. Pass to function
// 3. Construct return value
// 4. Return to caller
std::cout << values[0] << '\n';
return 0;
}
Without move semantics, this makes four copies! Let's optimize:
Steps 1 and 3: We must create objects here—there's no avoiding the construction of the argument and return value.
Step 2 (passing to function): We can optimize this with pass-by-reference. The argument exists for the entire function call, so references are safe. We can't use move semantics here because values is an lvalue—if we moved from it, values would become empty, breaking the code that prints it later.
Step 4 (returning to caller): We can't return by reference—the local return value is destroyed when the function ends, creating dangling references. However, since the return value is an rvalue (temporary), move semantics applies automatically! The expensive copy becomes a cheap move.
The optimal pattern for move-capable types: pass by const reference, return by value.
An optimized example
Here's the previous example optimized for performance:
#include <iostream>
#include <vector>
std::vector<int> doubleFirst(const std::vector<int>& input) // Pass by const ref
{
std::vector result{ input[0] * 2 };
return result; // Return by value (triggers move)
}
int main()
{
std::vector values{ 42 };
std::cout << doubleFirst(values)[0] << '\n'; // No copy on pass
std::cout << values[0] << '\n'; // values still intact
return 0;
}
Now step 2 uses a reference (no copy), and step 4 uses move semantics (cheap move instead of expensive copy).
Summary
Copy semantics: When copying a vector, all elements are duplicated into newly allocated memory. Both objects become independent copies.
When copies are wasteful: Copying temporary objects that are about to be destroyed wastes resources—we only need the data, not the object itself.
Move semantics: Instead of copying, ownership of data is transferred from one object to another. The source object is left in a valid but empty state.
Move triggers automatically: Move semantics activates when the source is an rvalue (temporary) and the type supports moving. No special syntax required.
Optimal pattern: For move-capable types like std::vector and std::string, pass by const reference but return by value. Passing by reference avoids copies, and returning by value triggers efficient moves.
Understanding move semantics explains why returning vectors by value is safe and efficient. We'll explore move semantics in greater depth in a later chapter.
Returning Vectors Efficiently - Quiz
Test your understanding of the lesson.
Practice Exercises
Returning Vectors from Functions
Practice returning std::vector by value from functions. Learn the correct pattern: pass by const reference, return by value. Move semantics ensures efficient data transfer.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!