CodeGym /Courses /C++ SELF /Pass by value: copies and the cost of copying

Pass by value: copies and the cost of copying

C++ SELF
Level 15, Lesson 0
Available

1. Introduction

When you start writing functions, it's very easy to think: "I passed a variable to a function — so the function works with my variable." That's a reasonable human expectation, especially if you are not yet familiar with the rules of the language. The problem is that C++ (like many languages) can pass arguments in different ways, and the simplest way — by value — actually makes a copy. And a copy can be almost free (int) or noticeably expensive (std::string, std::vector).

Today's goal is simple: form the habit of reading a function signature as a contract. See T x — mentally say: "Okay, there will be a copy." Then decide: "Is that fine, or am I accidentally running a copying factory?"

Pass by value: a copy enters the function

Pass by value looks like this: the parameter is declared as a regular variable — T x. Important to remember: a function parameter is a variable that lives inside the function. To create that variable it needs an initial value. When passed by value, that initial value is taken by copying the argument.

On an intuitive level it's like the situation: you gave your friend not your passport but a photocopy. The friend can draw a mustache on it with a marker as much as they like — your original will remain intact. With data it's the same: change the parameter inside the function — the original variable in main() does not change.

Let's draw a diagram (on a "high-level" human level, without deep memory details):

flowchart LR
    A["main(): variable a"] -->|copying| B["func(x): parameter x"]
    B --> C[modify x]
    C --> D[exit func, x disappears]
    A --> E[a in main remains unchanged]

Example 1: int by value — changed inside, unchanged outside

#include <iostream>

void set_zero(int x) {
    x = 0;
}

int main() {
    int a = 5;
    set_zero(a);
    std::cout << a << '\n'; // 5
}

Here x is a copy of a. Therefore a remains equal to 5.

Why int is fine but string/vector can be sad

Now comes an important fork in thinking. Passing by value is neither "bad" nor "good". It's a tool. It's ideal for small types where copying is like "picking up one pebble." But it becomes heavy for objects that contain a lot of data: copying becomes like "carrying a sack of potatoes," and sometimes — every time you call a function.

You can heuristically keep a table like this in mind:

Parameter type What roughly gets copied for T x Usually feels like
int, double, bool, char a few bytes almost free
std::string the characters of the string (in general) can be noticeable
std::vector<int> all elements of the vector often noticeable
std::vector<std::string> many strings, each string copied noticeable in time and memory

The phrase "in general" is important here: the standard library has optimizations, but we'll reason like engineers: if the size of the data grows, copying becomes more expensive. And if you accidentally copy data in every other function — the program starts slowing down for no obvious reason.

2. std::string by value: copying text and "invisible costs"

std::string looks like a single object, but logically it's a "container of characters." When you copy a string, it's like rewriting it entirely into a new notebook. Sometimes the string is short — and you won't even notice. But if the string has a thousand characters and you do this many times, copying becomes continuous background work.

Example 2: a function modifies the string, but the original string is unchanged

#include <iostream>
#include <string>

void add_exclamation(std::string s) {
    s += "!";
    std::cout << s << '\n'; // Hello!
}

int main() {
    std::string msg = "Hello";
    add_exclamation(msg);
    std::cout << msg << '\n'; // Hello
}

Here s is a copy of msg. It's convenient when you want to work with a copy. It's inconvenient when you accidentally thought you were changing the original.

Example 3: the "transformer function" — pass by value is appropriate

A very common and nice style: a function takes a value, creates a modified version and returns it. Then passing by value makes sense: "I received my own instance, modified it, and returned the result."

#include <iostream>
#include <string>

std::string make_tag(std::string s) {
    s = "[" + s + "]";
    return s;
}

int main() {
    std::string x = "TODO";
    std::cout << make_tag(x) << '\n'; // [TODO]
    std::cout << x << '\n';           // TODO
}

This reads as a pure transformation. Yes, there is a copy, but it matches the intent: you don't want a side effect of changing x.

2. std::vector by value: copying elements and O(N)

With std::vector it becomes even clearer. A vector is a container that can have many elements. Passing a vector by value means creating a new vector and copying elements into it. If there are N elements, copying is roughly proportional to N. Intuitively that's O(N): the larger the vector, the longer it takes.

That's why beginners often encounter the puzzle: "Why is my code fast with small data but suddenly slow with large data?" A very common answer: "Because you're copying the vector in every function."

Example 4: modifying a copy of a vector doesn't affect the original vector

#include <iostream>
#include <vector>

void push_demo(std::vector<int> v) {
    v.push_back(99);
    std::cout << v.size() << '\n'; // 4
}

int main() {
    std::vector<int> data{1, 2, 3};
    push_demo(data);
    std::cout << data.size() << '\n'; // 3
}

Outside, data remained the same. Inside push_demo() we changed only the copy v.

Example 5: copying a vector copies its elements

Let's show this not by timing (we haven't learned precise timing yet), but by meaning:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> a{1, 2, 3};
    std::vector<int> b = a; // copy of all elements

    b[0] = 100;
    std::cout << a[0] << '\n'; // 1
    std::cout << b[0] << '\n'; // 100
}

If b were just "another name for a", then a[0] would also become 100. But it does not. That means b is a separate container with its own elements.

3. Useful nuances: const, transformers and "hidden copies"

const T x — still a copy

Sometimes people want to "fix the situation" like this: "Okay, since the problem is modifications, I'll make the parameter const, and there will be no copy." It's an understandable attempt, but it doesn't work.

const forbids changing the local variable inside the function, but it does not cancel the fact that a local variable is created. So the copy is still made as before.

Example 6: const std::string s doesn't save you from copying

#include <iostream>
#include <string>

std::size_t len(const std::string s) {
    return s.size();
}

int main() {
    std::string text = "abcdef";
    std::cout << len(text) << '\n'; // 6
}

Here the string is still copied; you just cannot write s += "!" inside len().

In short: const is about preventing modification, not about performance.

When passing by value is a good choice

It's important not to swing to the other extreme: "by value = bad, never do it." That's like deciding: "since fire burns, I'll never cook again." Fire is useful when you know where the stove is and where the curtains are.

Pass by value is good in situations where, by intent, you want the function to work with an independent value. Usually this is one of two cases.

The first case — small simple types. If you have an int, passing by value looks fine, reads simpler, and you don't complicate the contract where it's not needed.

The second case — transformer functions where you intentionally receive a copy, modify it and return a new version. This makes code predictable: no unexpected changes "deep inside."

Example 7: transform a todo list and return a new one

We continue developing our console "To-Do List" application: for now it stores tasks as strings in a std::vector<std::string>. Now we will make a function that adds the prefix "[TODO] " to all tasks and returns a new list. Yes, copying happens — but that's the idea: we want a new list.

#include <string>
#include <vector>

std::vector<std::string> add_todo_prefix(std::vector<std::string> tasks) {
    for (std::string& t : tasks) {
        t = "[TODO] " + t;
    }
    return tasks;
}

Note: we don't change the original list "outside"; we build a new result. This is the "receive → transform → return" style.

Example 8: using the transformation in main()

#include <iostream>
#include <string>
#include <vector>

std::vector<std::string> add_todo_prefix(std::vector<std::string> tasks) {
    for (std::string& t : tasks) t = "[TODO] " + t;
    return tasks;
}

int main() {
    std::vector<std::string> tasks{"buy milk", "learn C++"};
    auto tagged = add_todo_prefix(tasks);

    std::cout << tasks[0] << '\n';  // buy milk
    std::cout << tagged[0] << '\n'; // [TODO] buy milk
}

This is a very readable contract: we don't change the input, we return a new value.

"Hidden copy": how to accidentally make the program heavier

Now let's look at a very practical fragment: a function that prints the task list. It's natural to write: "I'll pass the vector and print it." And write it by value. The code will work, but you'll copy the whole list every time. If the list has two tasks — no problem. If it has ten thousand tasks — that's a different story, and you'll start to suspect the computer took offense at you personally.

Example 9: print tasks, but with an unnecessary copy

#include <iostream>
#include <string>
#include <vector>

void print_tasks(std::vector<std::string> tasks) {
    std::cout << "Tasks: " << tasks.size() << '\n';
}

int main() {
    std::vector<std::string> tasks{"buy milk", "learn C++"};
    print_tasks(tasks); // output: Tasks: 2
}

The point of the function is "just to look." But by signature it means: "first make a copy of the whole list, then look." With small data you won't notice; with large data you will.

And here comes the main discipline of today's lecture: if you write T x, you must have a mental explanation why you need a copy. If your explanation is "I always write it this way" — that's a warning bell.

4. Common mistakes when passing by value

Mistake #1: expecting that T x will change the outside variable.
This is classic. Someone writes void clear(std::string s) { s = ""; }, calls clear(name), and the string is not cleared. The reason is simple: s is a copy. In such situations it's useful to say out loud: "A parameter is a local variable." If you need to change the original object, that must be expressed in the function contract (we'll cover contract details in the next lecture).

Mistake #2: "accidentally" copying std::vector and std::string in functions that only read.
These functions look harmless: print, count, sum, has_word. But if they take a large object by value, each check becomes "first copy the data, then look." This is especially nasty in loops: you can unknowingly do the copying N times, and the program becomes slow for no obvious reason.

Mistake #3: trying to fix copying by using const.
const is indeed useful, but it solves a different problem: it protects against accidental modification. It does not eliminate the copy. So const std::vector<int> v is "a copy you cannot modify", not "no copy".

Mistake #4: mixing two styles in one function: both "modify" and "return".
For example, a function receives std::string s (a copy), modifies it and also tries to "change the original string" in the caller (which is impossible with pass by value). Such code creates confusion: where does the result actually live? Get used to choosing one style: either the function purely returns a new value, or it explicitly modifies the passed object (but that's the next learning step).

Mistake #5: making an expensive copy "just for one check".
Sometimes people write: "I just wanted to know the size of the vector, but I passed it by value and called size()." It's like buying a refrigerator for one ice cube. If the read operation is small and copying is big, you're paying for something you don't need.

1
Task
C++ SELF, level 15, lesson 0
Locked
Zero in the radio
Zero in the radio
1
Task
C++ SELF, level 15, lesson 0
Locked
Exclamation in a Draft
Exclamation in a Draft
1
Task
C++ SELF, level 15, lesson 0
Locked
Draft of the list
Draft of the list
1
Task
C++ SELF, level 15, lesson 0
Locked
Report and tags
Report and tags
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION