Coming Soon
This lesson is currently being developed
Returning std::vector and an introduction to move semantics
Master dynamic arrays with the vector container.
What to Expect
Comprehensive explanations with practical examples
Interactive coding exercises to practice concepts
Knowledge quiz to test your understanding
Step-by-step guidance for beginners
Development Status
Content is being carefully crafted to provide the best learning experience
Preview
Early Preview Content
This content is still being developed and may change before publication.
16.5 — Returning std::vector and an introduction to move semantics
In this lesson, you'll learn how to return std::vector from functions efficiently, understand the basics of move semantics, and discover how modern C++ optimizations make returning large objects much more efficient than you might expect.
Returning vectors from functions
Returning std::vector from functions is a common pattern in C++ programming. Let's explore the different approaches and understand their implications.
Basic vector return
Here's the simplest way to return a vector from a function:
#include <vector>
#include <iostream>
std::vector<int> createSequence(int count)
{
std::vector<int> result;
for (int i = 1; i <= count; ++i)
{
result.push_back(i);
}
return result; // Return by value
}
std::vector<std::string> createNames()
{
std::vector<std::string> names = {"Alice", "Bob", "Charlie", "Diana"};
return names; // Return by value
}
int main()
{
std::vector<int> sequence = createSequence(5);
std::vector<std::string> names = createNames();
std::cout << "Sequence: ";
for (int num : sequence)
{
std::cout << num << " ";
}
std::cout << std::endl;
std::cout << "Names: ";
for (const std::string& name : names)
{
std::cout << name << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Sequence: 1 2 3 4 5
Names: Alice Bob Charlie Diana
This code looks like it should be inefficient (copying large vectors), but modern C++ compilers apply optimizations that make it very efficient.
Understanding the historical problem
Before C++11 and modern compiler optimizations, returning large objects like vectors was genuinely expensive:
// Pre-C++11 concern: This seemed to involve expensive copying
std::vector<int> expensiveFunction()
{
std::vector<int> bigVector(1000000, 42); // Large vector
return bigVector; // Seemed like expensive copy
}
// Pre-C++11 workaround: Return by reference parameter
void lessBadFunction(std::vector<int>& output)
{
output.clear();
output.resize(1000000, 42); // Modify the passed-in vector
}
This historical concern led to complicated workarounds, but modern C++ has solved these problems elegantly.
Copy elision and Return Value Optimization (RVO)
Modern compilers apply an optimization called Return Value Optimization (RVO):
#include <vector>
#include <iostream>
std::vector<int> demonstrateRVO()
{
std::vector<int> localVector = {1, 2, 3, 4, 5};
// Even though this looks like a copy operation,
// the compiler will likely optimize it away
return localVector;
}
// The compiler essentially transforms the above into something like:
// void demonstrateRVO(std::vector<int>& return_slot)
// {
// // Construct directly in the return location
// new (&return_slot) std::vector<int>({1, 2, 3, 4, 5});
// }
int main()
{
// No copying happens here thanks to RVO!
std::vector<int> result = demonstrateRVO();
std::cout << "Result: ";
for (int value : result)
{
std::cout << value << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Result: 1 2 3 4 5
The compiler constructs the vector directly in the location where it will be used, eliminating any copying.
Introduction to move semantics
Move semantics (introduced in C++11) provide a way to transfer ownership of resources instead of copying them. This is particularly important for expensive-to-copy objects like std::vector.
L-values vs R-values
To understand move semantics, you need to understand the difference between l-values and r-values:
#include <vector>
#include <iostream>
int main()
{
std::vector<int> vec1 = {1, 2, 3}; // vec1 is an l-value (has a name, can be on left of =)
std::vector<int> vec2;
vec2 = vec1; // Copy: vec1 is an l-value, so we copy
vec2 = std::vector<int>{4, 5, 6}; // Move: temporary object is r-value, so we move
vec2 = std::move(vec1); // Move: std::move converts l-value to r-value
std::cout << "vec1 size after move: " << vec1.size() << std::endl; // May be 0
std::cout << "vec2 contents: ";
for (int value : vec2)
{
std::cout << value << " ";
}
std::cout << std::endl;
return 0;
}
Output:
vec1 size after move: 0
vec2 contents: 1 2 3
How move semantics work with vectors
When you move a std::vector, you're transferring ownership of its internal data:
#include <vector>
#include <iostream>
#include <utility>
void demonstrateMove()
{
std::vector<int> source = {1, 2, 3, 4, 5};
std::cout << "Source size before move: " << source.size() << std::endl;
std::cout << "Source capacity before move: " << source.capacity() << std::endl;
// Move the vector (transfer ownership)
std::vector<int> destination = std::move(source);
std::cout << "Source size after move: " << source.size() << std::endl;
std::cout << "Destination size: " << destination.size() << std::endl;
std::cout << "Destination contents: ";
for (int value : destination)
{
std::cout << value << " ";
}
std::cout << std::endl;
// source is now in a valid but unspecified state
// It's safe to use, but don't rely on its contents
source.clear(); // Safe to call
source.push_back(99); // Safe to use again
std::cout << "Source after reuse: " << source[0] << std::endl;
}
int main()
{
demonstrateMove();
return 0;
}
Output:
Source size before move: 5
Source capacity before move: 5
Source size after move: 0
Destination size: 5
Destination contents: 1 2 3 4 5
Source after reuse: 99
Returning vectors efficiently
Return by value with move semantics
#include <vector>
#include <iostream>
// This function returns efficiently using move semantics
std::vector<int> createLargeVector(int size)
{
std::vector<int> result;
result.reserve(size); // Pre-allocate for efficiency
for (int i = 0; i < size; ++i)
{
result.push_back(i * i);
}
return result; // Move semantics kick in automatically
}
// You can also explicitly move local objects
std::vector<std::string> processStrings(std::vector<std::string> input)
{
// Modify the input vector
for (std::string& str : input)
{
str = "processed_" + str;
}
// Explicitly move the result (though usually not necessary)
return std::move(input);
}
int main()
{
auto numbers = createLargeVector(10);
std::cout << "Numbers: ";
for (int num : numbers)
{
std::cout << num << " ";
}
std::cout << std::endl;
std::vector<std::string> words = {"hello", "world"};
auto processed = processStrings(std::move(words)); // Move input to function
std::cout << "Original words size: " << words.size() << std::endl; // Likely 0
std::cout << "Processed: ";
for (const std::string& word : processed)
{
std::cout << word << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Numbers: 0 1 4 9 16 25 36 49 64 81
Original words size: 0
Processed: processed_hello processed_world
Factory functions
A common pattern is to create factory functions that return configured vectors:
#include <vector>
#include <iostream>
#include <random>
class VectorFactory
{
public:
static std::vector<int> createRange(int start, int end)
{
std::vector<int> result;
for (int i = start; i <= end; ++i)
{
result.push_back(i);
}
return result;
}
static std::vector<int> createRandom(int count, int min = 0, int max = 100)
{
std::vector<int> result;
result.reserve(count);
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(min, max);
for (int i = 0; i < count; ++i)
{
result.push_back(dis(gen));
}
return result;
}
static std::vector<double> createNormalized(const std::vector<int>& input)
{
std::vector<double> result;
result.reserve(input.size());
int max_val = *std::max_element(input.begin(), input.end());
for (int value : input)
{
result.push_back(static_cast<double>(value) / max_val);
}
return result;
}
};
int main()
{
auto range = VectorFactory::createRange(1, 10);
auto random = VectorFactory::createRandom(5, 1, 50);
auto normalized = VectorFactory::createNormalized(range);
std::cout << "Range: ";
for (int val : range) std::cout << val << " ";
std::cout << std::endl;
std::cout << "Random: ";
for (int val : random) std::cout << val << " ";
std::cout << std::endl;
std::cout << "Normalized: ";
for (double val : normalized) std::cout << val << " ";
std::cout << std::endl;
return 0;
}
Output:
Range: 1 2 3 4 5 6 7 8 9 10
Random: 23 7 45 12 38
Normalized: 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1
Advanced move semantics examples
Moving expensive objects
#include <vector>
#include <iostream>
#include <string>
struct ExpensiveObject
{
std::vector<std::string> data;
ExpensiveObject(int size)
{
std::cout << "Creating expensive object with " << size << " elements" << std::endl;
for (int i = 0; i < size; ++i)
{
data.push_back("Element_" + std::to_string(i));
}
}
// Copy constructor (expensive)
ExpensiveObject(const ExpensiveObject& other) : data(other.data)
{
std::cout << "Copying expensive object (" << data.size() << " elements)" << std::endl;
}
// Move constructor (cheap)
ExpensiveObject(ExpensiveObject&& other) noexcept : data(std::move(other.data))
{
std::cout << "Moving expensive object (" << data.size() << " elements)" << std::endl;
}
};
std::vector<ExpensiveObject> createExpensiveVector()
{
std::vector<ExpensiveObject> result;
// These will be moved, not copied
result.emplace_back(10);
result.emplace_back(5);
return result; // Return by move
}
int main()
{
std::cout << "Creating vector of expensive objects:" << std::endl;
auto expensiveVec = createExpensiveVector();
std::cout << "Vector contains " << expensiveVec.size() << " objects" << std::endl;
return 0;
}
Output:
Creating vector of expensive objects:
Creating expensive object with 10 elements
Creating expensive object with 5 elements
Vector contains 2 objects
Chaining vector operations
Move semantics enable efficient chaining of operations:
#include <vector>
#include <iostream>
#include <algorithm>
std::vector<int> filter(std::vector<int> vec, int threshold)
{
vec.erase(std::remove_if(vec.begin(), vec.end(),
[threshold](int x) { return x < threshold; }),
vec.end());
return vec; // Move return
}
std::vector<int> transform(std::vector<int> vec, int multiplier)
{
for (int& value : vec)
{
value *= multiplier;
}
return vec; // Move return
}
std::vector<int> sort(std::vector<int> vec)
{
std::sort(vec.begin(), vec.end(), std::greater<int>());
return vec; // Move return
}
int main()
{
std::vector<int> numbers = {5, 2, 8, 1, 9, 3, 7, 4, 6};
std::cout << "Original: ";
for (int num : numbers) std::cout << num << " ";
std::cout << std::endl;
// Chain operations efficiently using move semantics
auto result = sort(transform(filter(std::move(numbers), 4), 2));
std::cout << "After filter(>=4), transform(*2), sort(desc): ";
for (int num : result) std::cout << num << " ";
std::cout << std::endl;
std::cout << "Original size after move: " << numbers.size() << std::endl;
return 0;
}
Output:
Original: 5 2 8 1 9 3 7 4 6
After filter(>=4), transform(*2), sort(desc): 18 14 12 10 8
Original size after move: 0
Best practices for returning vectors
1. Return by value is usually correct
// ✅ Good: Return by value
std::vector<int> createData()
{
std::vector<int> result = {1, 2, 3};
return result; // Compiler optimizes this
}
// ❌ Usually unnecessary: Return by reference to parameter
void createData(std::vector<int>& output) // More complex interface
{
output = {1, 2, 3};
}
2. Trust the compiler optimizations
// ✅ Good: Simple and efficient
std::vector<std::string> getNames()
{
return {"Alice", "Bob", "Charlie"}; // Direct return
}
// ❌ Premature optimization: Usually not needed
std::vector<std::string> getNames_verbose()
{
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
return std::move(names); // Explicit move usually not needed
}
3. Use move when you want to transfer ownership
std::vector<int> consumeAndProcess(std::vector<int> data) // Takes by value for move
{
// Process data...
std::sort(data.begin(), data.end());
return data; // Return processed data
}
// Call it:
auto result = consumeAndProcess(std::move(myVector)); // Explicitly move
4. Pre-allocate when possible
std::vector<int> createSequence(int count)
{
std::vector<int> result;
result.reserve(count); // Avoid multiple reallocations
for (int i = 0; i < count; ++i)
{
result.push_back(i);
}
return result;
}
Common mistakes to avoid
1. Unnecessary std::move on return values
// ❌ Bad: Unnecessary std::move can prevent RVO
std::vector<int> badFunction()
{
std::vector<int> result = {1, 2, 3};
return std::move(result); // Can prevent optimization!
}
// ✅ Good: Let the compiler handle it
std::vector<int> goodFunction()
{
std::vector<int> result = {1, 2, 3};
return result; // Compiler optimizes automatically
}
2. Using moved-from objects
// ❌ Bad: Using vector after it's been moved from
std::vector<int> vec = {1, 2, 3};
auto other = std::move(vec);
std::cout << vec.size(); // Bad: vec is in unspecified state
// ✅ Good: Don't use moved-from objects (or reset them first)
std::vector<int> vec = {1, 2, 3};
auto other = std::move(vec);
vec.clear(); // Reset to known state
vec.push_back(42); // Now safe to use
3. Returning references to local objects
// ❌ Very bad: Returning reference to local object
const std::vector<int>& badFunction() // Dangling reference!
{
std::vector<int> local = {1, 2, 3};
return local; // local is destroyed when function returns
}
// ✅ Good: Return by value
std::vector<int> goodFunction()
{
std::vector<int> local = {1, 2, 3};
return local; // Safe and efficient
}
Summary
Returning std::vector from functions is both safe and efficient in modern C++:
- Return Value Optimization (RVO): Compilers eliminate unnecessary copies
- Move semantics: Automatically transfer ownership instead of copying
- Best practice: Return by value and trust the compiler
- std::move: Use when you want to explicitly transfer ownership
- Factory pattern: Create and return configured vectors efficiently
Key points:
- Don't be afraid to return vectors by value
- Modern C++ handles the efficiency concerns automatically
- Use std::move when you want to transfer ownership
- Avoid returning references to local objects
- Pre-allocate space when you know the final size
In the next lesson, you'll learn about using arrays and loops together effectively.
Quiz
- What is Return Value Optimization (RVO) and how does it help with vector returns?
- What's the difference between copying and moving a std::vector?
- When should you use std::move explicitly when returning vectors?
- What happens to a vector after it has been moved from?
- Why is
return std::move(localVector)
usually not recommended?
Practice exercises
Try these exercises to practice returning vectors and move semantics:
-
Vector factory: Create a class with static methods that return different types of configured vectors (fibonacci sequence, prime numbers, random numbers). Test the efficiency by creating large vectors.
-
Processing pipeline: Write functions that take a vector, process it, and return the result. Chain several operations together using move semantics to avoid unnecessary copies.
-
Move demonstration: Write a program that demonstrates the difference between copying and moving vectors. Create a vector with a million elements and time both operations.
-
Factory with parameters: Create a function that takes configuration parameters and returns a vector created according to those parameters. Practice both passing parameters and returning the result efficiently.
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions