Passing Arguments: Value, Reference and Move

How you declare a parameter decides whether the function gets a copy, read-only access, or the right to change the caller's object. A few simple rules cover almost every case and keep C++ programs both correct and fast.

The guidelines

The function…Parameter typeExample
Only reads a cheap value (numbers, characters, small structs)By value: Tdouble area(double w, double h)
Only reads an expensive object (strings, vectors, big structs)By const reference: const T&int count(const std::string& text)
Modifies the caller's objectBy reference: T&void sort_names(std::vector<std::string>& v)
Needs its own copy to keep (e.g. to store)By value, then std::move itvoid set_name(std::string n)
Reads a string or array of any kindstd::string_view / std::spanlesson 19

Seeing the copies

This small class prints whenever it's copied, which makes the difference visible:

C++
#include <iostream>
#include <string>
#include <utility>

struct Tracker {
    std::string name;
    explicit Tracker(std::string n) : name(std::move(n)) {}
    Tracker(const Tracker& other) : name(other.name) { std::cout << "  (copied " << name << ")\n"; }
};

void by_value(Tracker t) { std::cout << "by value: " << t.name << '\n'; }
void by_const_ref(const Tracker& t) { std::cout << "by const&: " << t.name << '\n'; }
void by_ref(Tracker& t) { t.name += "!"; }

int main() {
    Tracker big{"report"};
    by_value(big);
    by_const_ref(big);
    by_ref(big);
    std::cout << "after by_ref: " << big.name << '\n';
}
Output
  (copied report)
by value: report
by const&: report
after by_ref: report!

Only the by-value call made a copy. Classes and constructors are explained in lessons 31–32; for now, watch the "copied" line.

Move semantics in brief

Copying a long string or a big vector duplicates all its data. Moving transfers the data instead — the destination takes over the source's memory, leaving the source valid but empty (or otherwise unspecified). std::move says "I'm done with this; its contents may be taken":

C++
#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::vector<std::string> names;
    std::string name = "a fairly long name that would be expensive to copy";

    names.push_back(name);              // copies: name still holds its text
    names.push_back(std::move(name));   // moves: the text is transferred
    std::cout << names.size() << " names, original now has " << name.size() << " characters\n";
}
Output
2 names, original now has 0 characters

After a move, only assign a new value to the moved-from object or let it be destroyed — don't rely on what it contains. Lesson 35 explains how classes support moving.

Taking a parameter "by value and move"

When a function stores its argument (for example a setter or a constructor), take it by value and move it into place. Callers passing a temporary then pay for no copy at all, and callers passing a variable pay for exactly one:

C++
#include <iostream>
#include <string>
#include <utility>

class Contact {
public:
    void set_email(std::string email) { email_ = std::move(email); }
    const std::string& email() const { return email_; }
private:
    std::string email_;
};

int main() {
    Contact c;
    c.set_email("sam@example.com");          // temporary: moved all the way, no copy
    std::string backup = "sam.rivera@example.org";
    c.set_email(backup);                      // variable: copied once into the parameter
    std::cout << c.email() << " / " << backup << '\n';
}
Output
sam.rivera@example.org / sam.rivera@example.org

Returning objects is cheap

Returning a local object by value doesn't copy it: the compiler constructs it directly in the caller's variable (copy elision), or moves it. So write functions that return results by value — don't use output parameters just to "avoid a copy":

C++
#include <iostream>
#include <vector>

std::vector<int> squares(int n) {
    std::vector<int> result;
    for (int i = 1; i <= n; ++i) result.push_back(i * i);
    return result;            // no copy of the vector's contents
}

int main() {
    auto s = squares(6);
    for (int x : s) std::cout << x << ' ';
    std::cout << '\n';
}
Output
1 4 9 16 25 36

Don't return references to locals

Returning by value is safe; returning a reference or pointer to a local variable is a dangling-reference bug (lesson 13). When in doubt, return by value.

Try it yourself

Write a function that takes a const reference to a list of words and returns a new vector containing only the long ones.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
elephant
hippopotamus