Coming Soon
This lesson is currently being developed
Passing std::vector
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.4 — Passing std::vector
In this lesson, you'll learn the different ways to pass std::vector to functions, when to use each method, and how to write efficient and safe code when working with vectors as function parameters.
Why passing vectors matters
When you pass a std::vector to a function, you need to consider:
- Performance: Avoid unnecessary copying of large amounts of data
- Intent: Communicate whether the function will modify the vector
- Safety: Prevent accidental modifications when they're not intended
Understanding how to pass vectors correctly is crucial for writing efficient C++ programs.
The five ways to pass std::vector
1. Pass by value (copy)
Passing by value creates a complete copy of the vector:
#include <vector>
#include <iostream>
// Pass by value - creates a copy
void printVector(std::vector<int> vec) // vec is a copy
{
std::cout << "Vector contents: ";
for (int value : vec)
{
std::cout << value << " ";
}
std::cout << std::endl;
// Modifying vec doesn't affect the original
vec[0] = 999;
std::cout << "After modifying copy: " << vec[0] << std::endl;
}
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::cout << "Original first element: " << numbers[0] << std::endl;
printVector(numbers);
std::cout << "Original after function call: " << numbers[0] << std::endl;
return 0;
}
Output:
Original first element: 1
Vector contents: 1 2 3 4 5
After modifying copy: 999
Original after function call: 1
When to use pass by value:
- When you need to modify the vector without affecting the original
- When the vector is small (performance isn't a concern)
- When implementing algorithms that need their own copy
2. Pass by const reference (read-only)
This is the most common and efficient way to pass vectors you don't need to modify:
#include <vector>
#include <iostream>
// Pass by const reference - no copy, cannot modify
void printVector(const std::vector<int>& vec) // vec refers to original
{
std::cout << "Vector size: " << vec.size() << std::endl;
std::cout << "Vector contents: ";
for (int value : vec)
{
std::cout << value << " ";
}
std::cout << std::endl;
// vec[0] = 999; // Error! Cannot modify const reference
}
double calculateAverage(const std::vector<double>& scores)
{
if (scores.empty())
{
return 0.0;
}
double sum = 0.0;
for (double score : scores)
{
sum += score;
}
return sum / scores.size();
}
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<double> testScores = {85.5, 92.0, 78.5, 90.0, 88.5};
printVector(numbers);
double average = calculateAverage(testScores);
std::cout << "Average score: " << average << std::endl;
return 0;
}
Output:
Vector size: 5
Vector contents: 1 2 3 4 5
Average score: 86.9
When to use const reference:
- When you only need to read from the vector
- For performance (avoids copying)
- To show intent that the function won't modify the vector
3. Pass by non-const reference (modify original)
Use this when you need to modify the original vector:
#include <vector>
#include <iostream>
// Pass by non-const reference - can modify original
void doubleValues(std::vector<int>& vec)
{
for (int& value : vec) // Note: reference to allow modification
{
value *= 2;
}
}
void addElement(std::vector<std::string>& vec, const std::string& newElement)
{
vec.push_back(newElement);
}
void sortVector(std::vector<int>& vec)
{
// Simple bubble sort for demonstration
for (std::size_t i = 0; i < vec.size(); ++i)
{
for (std::size_t j = i + 1; j < vec.size(); ++j)
{
if (vec[i] > vec[j])
{
std::swap(vec[i], vec[j]);
}
}
}
}
int main()
{
std::vector<int> numbers = {3, 1, 4, 1, 5};
std::vector<std::string> names = {"Alice", "Bob"};
std::cout << "Original numbers: ";
for (int num : numbers) std::cout << num << " ";
std::cout << std::endl;
doubleValues(numbers);
std::cout << "After doubling: ";
for (int num : numbers) std::cout << num << " ";
std::cout << std::endl;
sortVector(numbers);
std::cout << "After sorting: ";
for (int num : numbers) std::cout << num << " ";
std::cout << std::endl;
addElement(names, "Charlie");
std::cout << "Names: ";
for (const std::string& name : names) std::cout << name << " ";
std::cout << std::endl;
return 0;
}
Output:
Original numbers: 3 1 4 1 5
After doubling: 6 2 8 2 10
After sorting: 2 2 6 8 10
Names: Alice Bob Charlie
When to use non-const reference:
- When you need to modify the original vector
- For performance (avoids copying)
- When the function's purpose is to change the vector
4. Pass by pointer
Less common in modern C++, but sometimes useful:
#include <vector>
#include <iostream>
// Pass by pointer - can modify original, can be null
void clearIfNotEmpty(std::vector<int>* vec)
{
if (vec != nullptr && !vec->empty()) // Check for null pointer
{
vec->clear();
}
}
void printVectorInfo(const std::vector<int>* vec)
{
if (vec == nullptr)
{
std::cout << "Null vector pointer!" << std::endl;
return;
}
std::cout << "Vector size: " << vec->size() << std::endl;
if (!vec->empty())
{
std::cout << "First element: " << (*vec)[0] << std::endl;
std::cout << "Last element: " << vec->back() << std::endl;
}
}
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int>* vecPtr = &numbers;
printVectorInfo(vecPtr);
clearIfNotEmpty(vecPtr);
printVectorInfo(vecPtr);
// Demonstrate null pointer handling
printVectorInfo(nullptr);
return 0;
}
Output:
Vector size: 5
First element: 1
Last element: 5
Vector size: 0
Null vector pointer!
When to use pointers:
- When the vector might be null/optional
- When interfacing with C-style APIs
- In rare cases where you need to reassign the vector itself
5. Pass by move (C++11+)
For transferring ownership of the vector:
#include <vector>
#include <iostream>
#include <utility>
// Pass by move - takes ownership of the vector
std::vector<int> processAndReturn(std::vector<int> vec) // Intentionally by value for move
{
// Process the vector
for (int& value : vec)
{
value *= 2;
}
// Add some elements
vec.push_back(100);
vec.push_back(200);
return vec; // Return by value (will be moved)
}
void consumeVector(std::vector<int>&& vec) // Rvalue reference
{
std::cout << "Consuming vector with " << vec.size() << " elements: ";
for (int value : vec)
{
std::cout << value << " ";
}
std::cout << std::endl;
// vec is now "consumed" - don't use the original after this
}
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::cout << "Original size: " << numbers.size() << std::endl;
// Move the vector to the function
std::vector<int> result = processAndReturn(std::move(numbers));
std::cout << "Original size after move: " << numbers.size() << std::endl; // May be 0
std::cout << "Result: ";
for (int value : result) std::cout << value << " ";
std::cout << std::endl;
// Consume the result vector
consumeVector(std::move(result));
return 0;
}
Output:
Original size: 5
Original size after move: 0
Result: 2 4 6 8 10 100 200
Consuming vector with 7 elements: 2 4 6 8 10 100 200
When to use move semantics:
- When transferring ownership of large vectors
- To avoid expensive copies in performance-critical code
- When implementing move constructors and assignment operators
Performance comparison
Let's demonstrate the performance differences:
#include <vector>
#include <iostream>
#include <chrono>
// Pass by value (expensive)
void processByValue(std::vector<int> vec)
{
// Just access first element to prevent optimization
if (!vec.empty()) vec[0] = vec[0];
}
// Pass by const reference (efficient)
void processByConstRef(const std::vector<int>& vec)
{
// Just access first element
if (!vec.empty()) int dummy = vec[0];
}
int main()
{
// Create a large vector
std::vector<int> largeVector(1000000, 42);
// Time pass by value
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100; ++i)
{
processByValue(largeVector);
}
auto end = std::chrono::high_resolution_clock::now();
auto valueTime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
// Time pass by const reference
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100; ++i)
{
processByConstRef(largeVector);
}
end = std::chrono::high_resolution_clock::now();
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "Pass by value time: " << valueTime.count() << " microseconds" << std::endl;
std::cout << "Pass by reference time: " << refTime.count() << " microseconds" << std::endl;
std::cout << "Reference is " << (valueTime.count() / (refTime.count() + 1))
<< "x faster" << std::endl;
return 0;
}
Output (example):
Pass by value time: 45231 microseconds
Pass by reference time: 15 microseconds
Reference is 3015x faster
Best practices and guidelines
Choose the right parameter type
// ✅ Read-only access - use const reference
void printStats(const std::vector<int>& data);
// ✅ Modify original - use non-const reference
void sortData(std::vector<int>& data);
// ✅ Need a copy to work with - use value
std::vector<int> getProcessedCopy(std::vector<int> data);
// ❌ Inefficient for large vectors
void printStats(std::vector<int> data); // Creates expensive copy
// ❌ Unclear intent
void processData(std::vector<int>& data); // Will this modify data?
Function naming and documentation
// Good: Function name indicates intent
void displayVector(const std::vector<int>& vec); // Won't modify
void reverseVector(std::vector<int>& vec); // Will modify
std::vector<int> getSortedVector(std::vector<int> vec); // Returns processed copy
// Better: Add comments for complex cases
/**
* Removes all elements that match the predicate
* @param vec Vector to modify (elements may be removed)
* @param shouldRemove Function that returns true for elements to remove
*/
template<typename Predicate>
void removeIf(std::vector<int>& vec, Predicate shouldRemove);
Template functions with vectors
#include <vector>
#include <iostream>
// Generic function that works with any vector type
template<typename T>
void printVector(const std::vector<T>& vec)
{
std::cout << "Vector contents: ";
for (const T& element : vec)
{
std::cout << element << " ";
}
std::cout << std::endl;
}
// Generic function for modifying vectors
template<typename T>
void clearAndResize(std::vector<T>& vec, std::size_t newSize, const T& fillValue)
{
vec.clear();
vec.resize(newSize, fillValue);
}
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<std::string> words = {"hello", "world"};
printVector(numbers); // Works with int
printVector(words); // Works with string
clearAndResize(numbers, 3, 42);
printVector(numbers);
return 0;
}
Output:
Vector contents: 1 2 3 4 5
Vector contents: hello world
Vector contents: 42 42 42
Common mistakes and pitfalls
Mistake 1: Unnecessary copying
// ❌ Bad: Unnecessary copy for read-only operation
double getAverage(std::vector<double> scores) // Copies entire vector!
{
double sum = 0;
for (double score : scores) sum += score;
return sum / scores.size();
}
// ✅ Good: Use const reference
double getAverage(const std::vector<double>& scores)
{
double sum = 0;
for (double score : scores) sum += score;
return sum / scores.size();
}
Mistake 2: Forgetting const when appropriate
// ❌ Bad: Missing const - unclear intent
void displayResults(std::vector<int>& results) // Might this modify results?
{
for (int result : results)
{
std::cout << result << std::endl;
}
}
// ✅ Good: const shows intent clearly
void displayResults(const std::vector<int>& results)
{
for (int result : results)
{
std::cout << result << std::endl;
}
}
Mistake 3: Inconsistent parameter types
// ❌ Bad: Inconsistent parameter types
void processData(const std::vector<int>& input, std::vector<int> output) // Why different?
{
// Process input into output
}
// ✅ Good: Consistent and clear intent
void processData(const std::vector<int>& input, std::vector<int>& output)
{
// Process input into output
}
Summary
Choosing the right way to pass std::vector is crucial for performance and code clarity:
- const reference (
const std::vector<T>&
): Most common choice for read-only access - non-const reference (
std::vector<T>&
): When you need to modify the original vector - by value (
std::vector<T>
): When you need a copy to work with - by pointer (
std::vector<T>*
): Rarely needed, mainly for optional parameters - by move (
std::vector<T>&&
): For transferring ownership
General guidelines:
- Default to
const std::vector<T>&
for read-only functions - Use
std::vector<T>&
when you need to modify the original - Use
std::vector<T>
by value only when you need a copy - Name functions clearly to indicate their intent
- Add const whenever possible to prevent accidental modifications
In the next lesson, you'll learn about returning vectors from functions and an introduction to move semantics.
Quiz
- What are the performance implications of passing a large vector by value vs by const reference?
- When would you choose to pass a vector by non-const reference?
- What's the advantage of using const reference over non-const reference when you don't need to modify the vector?
- How do you pass a vector that might be null or optional?
- What does
std::vector<T>&&
represent and when would you use it?
Practice exercises
Try these exercises to practice passing vectors to functions:
-
Statistics calculator: Write functions that take a vector of doubles and return the minimum, maximum, sum, and average. Use appropriate parameter types for each function.
-
Vector modifier: Write a function that removes all negative numbers from a vector of integers. Then write another version that returns a new vector with negatives removed (without modifying the original).
-
Template practice: Create a template function that can find the maximum element in any type of vector. Test it with vectors of integers, doubles, and strings.
-
Performance test: Create a large vector and write two versions of a simple processing function - one that passes by value and one by const reference. Time both approaches and compare the results.
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions