Skip to content
C Codeloom
C++

C++ Functions, References, and Pass-By-Value

Write C++ functions that pass data correctly: by value, by reference, by const reference, with overloading, default arguments, and clear ownership.

·5 min read · By Yash Kesharwani
Beginner 9 min read

What you'll learn

  • Choose pass-by-value, reference, or const-reference correctly
  • Use default arguments and overloading without abuse
  • Return values cleanly and avoid dangling references
  • Declare functions with auto trailing return types
  • Spot the cost of copies in hot paths

Prerequisites

  • C++ syntax basics — see /blog/cpp-control-flow

Functions are how C++ programs are composed. The choices you make at the signature level — value, reference, const reference — decide both performance and correctness.

Function basics

A C++ function has a return type, a name, a parameter list, and a body.

int add(int a, int b) {
    return a + b;
}

Declare functions in headers so other translation units can call them. Define them in .cpp files unless they are templates or short inline helpers.

// math.h
int add(int a, int b);

// math.cpp
#include "math.h"
int add(int a, int b) { return a + b; }

Pass by value

Parameters declared without & are copies. Modifications inside the function do not affect the caller.

void increment(int x) { ++x; }  // caller's x unchanged

int n = 5;
increment(n);  // n is still 5

Pass small, trivially copyable types (int, double, pointers, small structs) by value. Copying them is essentially free.

Pass by reference

A reference is an alias for the caller’s variable. Marking a parameter T& lets the function mutate the original.

void increment(int& x) { ++x; }

int n = 5;
increment(n);  // n is now 6

References cannot be null and cannot be rebound after initialization. They are safer than pointers when you need a non-owning, always-valid handle.

Pass by const reference

For larger types — std::string, std::vector, custom classes — pass by const T&. You avoid the copy and the function cannot mutate the caller’s value.

#include <string>

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

Rule of thumb: if the type is bigger than two pointers, pass by const&. The standard string and container types definitely qualify.

Returning values

Return by value for most functions. Modern compilers apply return-value optimization (RVO) and move semantics so returning a std::vector<int> is cheap.

std::vector<int> make_range(int n) {
    std::vector<int> out;
    out.reserve(n);
    for (int i = 0; i < n; ++i) out.push_back(i);
    return out;  // moved, not copied
}

Never return a reference to a local. The local dies at the closing brace and you hand the caller a dangling reference.

const std::string& bad() {
    std::string s = "oops";
    return s;       // dangling — undefined behavior on use
}

Default arguments

You can supply defaults from the rightmost parameter inward. Declare them in the header only; the definition omits them.

// header
void log(const std::string& msg, int level = 1);

// source
void log(const std::string& msg, int level) { /* ... */ }

Default arguments are convenient but they make overload resolution trickier. If a function has more than two defaults, consider an options struct instead.

Function overloading

Multiple functions can share a name as long as their parameter lists differ.

int max(int a, int b)       { return a > b ? a : b; }
double max(double a, double b) { return a > b ? a : b; }

Overloading is resolved at compile time based on the argument types. Avoid overloads that differ only in subtle conversions; the call site becomes a puzzle.

Trailing return types and auto

For complex return types, the trailing form is often clearer.

auto make_pair(int a, int b) -> std::pair<int, int> {
    return {a, b};
}

In simple cases you can let the compiler deduce the return type:

auto square(int x) { return x * x; }

Use return deduction sparingly in public APIs; explicit types document intent.

Inline and constexpr functions

inline was originally a hint to inline the body. Today it mostly tells the linker that multiple definitions are allowed, which is necessary for functions defined in headers.

inline int double_it(int x) { return x * 2; }

constexpr allows the function to run at compile time when arguments are constants.

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int twenty_four = factorial(4);  // computed at compile time

Lambdas as inline functions

Lambdas create anonymous function objects. Use them for small callbacks.

#include <algorithm>
#include <vector>

std::vector<int> v{3, 1, 4, 1, 5};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

The square brackets are the capture list. [&] captures surrounding variables by reference, [=] by value. Be deliberate: capturing by reference into a callback that outlives the scope is another dangling-reference trap.

A worked example

A function that trims whitespace from a string, mutating in place via reference:

#include <string>
#include <cctype>

void trim(std::string& s) {
    auto is_space = [](unsigned char c) { return std::isspace(c); };
    while (!s.empty() && is_space(s.back()))  s.pop_back();
    auto it = s.begin();
    while (it != s.end() && is_space(*it))    ++it;
    s.erase(s.begin(), it);
}

The reference parameter signals mutation. If you preferred a pure version, return std::string by value.

Common pitfalls

  • Returning references or pointers to locals.
  • Accidentally copying a large parameter by forgetting &.
  • Overloading on types that convert to each other implicitly.
  • Capturing a lambda by reference and storing it past the scope.

What is next

Functions usually need to manipulate memory directly at some point. Read Pointers and Memory next, then Classes, Constructors, and the Rule of Three to build your own types.

Wrap up

Pass small types by value, large types by const&, mutate via &, return by value and trust the compiler. Reach for overloads and defaults when they clarify; avoid them when they obscure. Signatures are a contract — write them with intent.