Coming Soon

This lesson is currently being developed

Passing std::vector

Master dynamic arrays with the vector container.

Dynamic arrays: std::vector
Chapter
Beginner
Difficulty
35min
Estimated Time

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

In Progress

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

  1. What are the performance implications of passing a large vector by value vs by const reference?
  2. When would you choose to pass a vector by non-const reference?
  3. What's the advantage of using const reference over non-const reference when you don't need to modify the vector?
  4. How do you pass a vector that might be null or optional?
  5. What does std::vector<T>&& represent and when would you use it?

Practice exercises

Try these exercises to practice passing vectors to functions:

  1. 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.

  2. 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).

  3. 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.

  4. 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.

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion