Last updated: December 2025

C++ Functions: Syntax, Overloading, Lambdas, Templates & Recursion — Complete Guide (2025)

By CoodeVerse Editorial Team ✓ 2025 Verified ⏱ 22 min read 🎯 Beginner–Intermediate 📦 C++11/17/20
Difficulty:
Beginner–Intermediate — Prerequisites: Variables & Types

⚡ Quick Answer: Functions in C++

Functions are the fundamental building blocks of every C++ program. Understanding them — from the difference between pass-by-value and pass-by-reference to lambdas and function templates — is essential for writing clean, efficient, testable code. This guide covers every function concept with annotated code and six complete DSA examples.

📐

Anatomy & Syntax

Declaration vs definition

🔗

Parameter Passing

value / ref / const-ref / ptr

🔀

Overloading

Same name, diff params

🎛️

Default Arguments

Preset parameter values

λ

Lambdas

Anonymous inline functions

🧬

Templates & constexpr

Generic + compile-time

🔄

Recursion

Base case + recursive case

🏆

DSA Examples

6 algorithms

📐 Section 1

Function Anatomy — Declaration vs Definition

C++ function anatomy
int add ( int a, int b ) { return a + b; }
↑ return type ↑ name ↑ parameters ↑ body
function_anatomy.cpp — declaration vs definition, void vs returningC++
#include <iostream>
#include <string>
using namespace std;

// ── Declaration (prototype) — needed if definition is AFTER main ─
int  add(int a, int b);
void greet(const string& name);

int main() {
    cout << add(3, 5) << endl;  // 8
    greet("Alice");              // Hello, Alice!
    return 0;
}

// ── Definitions (can come after main if prototype exists above) ──
int add(int a, int b) {
    return a + b;
}

void greet(const string& name) {
    cout << "Hello, " << name << "!\n";
    // no return statement — void function
}
Output
8
Hello, Alice!
Declaration vs definition: A declaration tells the compiler what a function looks like (signature). A definition tells it how it works (body). In multi-file projects, declarations go in .hpp headers, definitions go in .cpp files. Within a single file, put the definition before the first call — then no prototype is needed.
🔗 Section 2

Parameter Passing — All 5 Modes Compared

By value
Copy made. Cannot modify original.
void f(int x)
Use for: small types (int, double, bool)
By reference (&)
Alias to original. Can modify.
void f(int& x)
Use for: output parameters
By const ref
Read-only alias. No copy.
void f(const string& s)
Use for: large input-only params
By pointer
Can be null. Requires *deref.
void f(int* x)
Use for: optional params, C APIs
By move (&&)
Transfer ownership. Source empty.
void f(vector<int>&& v)
Use for: factory functions, sinks
parameter_modes.cpp — all 5 modes demonstratedC++
#include <iostream>
#include <vector>
#include <utility>  // std::move
using namespace std;

// 1. By VALUE — cannot modify caller's variable
void byValue(int x) { x = 99; }   // caller's x unchanged

// 2. By REFERENCE — modifies caller's variable
void byRef(int& x) { x = 99; }   // caller's x IS changed

// 3. By CONST REF — read-only, no copy (efficient for strings/vectors)
void byConstRef(const string& s) {
    cout << "len=" << s.length() << endl;  // read only
}

// 4. By POINTER — can be null
void byPointer(int* p) {
    if (p) *p = 99;  // always null-check
}

// 5. By MOVE — transfer ownership, caller's object is emptied
void byMove(vector<int>&& v) {
    vector<int> local = move(v);  // steal v's data
    cout << "Received " << local.size() << " elements\n";
}

int main() {
    int a = 5;
    byValue(a);   cout << a << endl;  // 5 — unchanged
    byRef(a);     cout << a << endl;  // 99 — changed
    byConstRef("Hello World");        // len=11, no copy

    int b = 10;
    byPointer(&b); cout << b << endl; // 99

    vector<int> v = {1,2,3};
    byMove(move(v));
    cout << v.size() << endl;        // 0 — moved-out
    return 0;
}
Output
5
99
len=11
99
Received 3 elements
0
Rule of thumb for parameter passing: Pass primitives (int, double, bool) by value. Pass anything else (string, vector, structs) by const& for input-only, by & for output, and by value (then std::move) when you need to store a copy.
🔀 Section 3

Function Overloading — Rules & Pitfalls

overloading.cpp — overloads + common pitfallsC++
#include <iostream>
#include <string>
using namespace std;

// Three overloads — same name, different parameter types
int    add(int a, int b)          { return a + b; }
double add(double a, double b)    { return a + b; }
string add(const string& a, const string& b) { return a + b; }

// Overloading on number of parameters
int multiply(int a, int b)          { return a * b; }
int multiply(int a, int b, int c)  { return a * b * c; }

// ⚠ Return type alone does NOT distinguish overloads — compile error!
// int  getData();   ← ERROR: ambiguous with the double version below
// double getData();

int main() {
    cout << add(3, 5)             << endl;  // 8      — int version
    cout << add(3.0, 5.0)         << endl;  // 8      — double version
    cout << add("Hello", " World") << endl;  // Hello World
    cout << multiply(2, 3)         << endl;  // 6
    cout << multiply(2, 3, 4)      << endl;  // 24
    return 0;
}
Output
8
8
Hello World
6
24
FactorDistinguishes overloads?Example
Parameter types✓ Yesf(int) vs f(double)
Parameter count✓ Yesf(int) vs f(int, int)
Parameter order✓ Yesf(int, double) vs f(double, int)
Return type only✗ No — compile errorint f() vs double f()
const qualifier on ref✓ Yesf(int&) vs f(const int&)
🎛️ Section 4

Default Arguments

default_args.cpp — rules and real-world exampleC++
#include <iostream>
#include <string>
using namespace std;

// Default arguments must be RIGHT-TO-LEFT
// ✓ correct: rightmost params get defaults first
void log(const string& msg,
          int           level     = 1,
          bool          timestamp = true)
{
    if (timestamp) cout << "[TS] ";
    cout << "L" << level << ": " << msg << endl;
}

// ✗ ILLEGAL: non-trailing default
// void bad(int x = 0, int y);  ← compile error

// Real-world: connection with sensible defaults
bool connect(const string& host,
              int           port    = 443,
              bool          secure  = true,
              int           timeout = 30)
{
    cout << (secure ? "https" : "http") << "://"
         << host << ":" << port
         << " (timeout=" << timeout << "s)\n";
    return true;
}

int main() {
    log("App started");           // level=1, timestamp=true
    log("Debug info", 3);         // level=3, timestamp=true
    log("No stamp", 1, false);    // timestamp=false

    connect("api.example.com");    // all defaults
    connect("api.example.com", 80, false);  // http
    return 0;
}
Output
[TS] L1: App started
[TS] L3: Debug info
L1: No stamp
https://api.example.com:443 (timeout=30s)
http://api.example.com:80 (timeout=30s)
λ Section 5

Lambda Functions & std::function

lambdas.cpp — capture, STL integration, std::functionC++11+
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;

int main() {
    // ── Basic lambda ───────────────────────────────────────────
    auto square = [](int x) { return x * x; };
    cout << square(5) << endl;  // 25

    // ── Capture by value [=] and by reference [&] ─────────────
    int factor = 3;
    auto multiply = [factor](int x) { return x * factor; };  // copies factor
    auto addToFactor = [&factor](int x) { factor += x; };       // modifies factor

    cout << multiply(4) << endl;  // 12
    addToFactor(10); cout << factor << endl;  // factor = 13

    // ── Lambda in STL algorithms ───────────────────────────────
    vector<int> v = {5, 2, 8, 1, 9, 3};

    // Sort descending
    sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
    for (int x : v) cout << x << " ";  cout << endl;  // 9 8 5 3 2 1

    // Count elements greater than 4
    int count = count_if(v.begin(), v.end(), [](int x){ return x > 4; });
    cout << "Count > 4: " << count << endl;  // 3

    // ── std::function — type-erased callable ──────────────────
    function<int(int,int)> op;
    op = [](int a, int b) { return a + b; };
    cout << op(3, 5) << endl;   // 8
    op = [](int a, int b) { return a * b; };
    cout << op(3, 5) << endl;   // 15

    return 0;
}
Output
25
12
13
9 8 5 3 2 1
Count > 4: 3
8
15
🧬 Section 6

Function Templates & constexpr

templates_constexpr.cppC++14/17
#include <iostream>
#include <string>
using namespace std;

// ── Function template — works for any comparable type ─────────
template<typename T>
const T& maxVal(const T& a, const T& b) {
    return a > b ? a : b;
}

// ── Template with multiple type parameters ────────────────────
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
}

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

// ── inline — zero-overhead small functions ────────────────────
inline int square(int x) noexcept { return x * x; }

// ── [[nodiscard]] — warn if return value is ignored ───────────
[[nodiscard]] int computeChecksum(const string& s) noexcept {
    int sum = 0;
    for (char c : s) sum += c;
    return sum;
}

int main() {
    // Template: compiler generates maxVal and maxVal
    cout << maxVal(10, 20)        << endl;  // 20
    cout << maxVal(3.14, 2.71)   << endl;  // 3.14
    cout << maxVal(string("Z"), string("A")) << endl;  // Z

    // Mixed types
    cout << add(3, 4.5) << endl;  // 7.5 (int+double → double)

    // constexpr: computed at compile time — same as a constant
    constexpr long long f10 = factorial(10);  // compile-time!
    cout << f10 << endl;  // 3628800

    cout << square(7) << endl;  // 49

    auto cs = computeChecksum("hello");  // [[nodiscard]] — must use
    cout << cs << endl;
    return 0;
}
Output
20
3.14
Z
7.5
3628800
49
532
🔄 Section 7

Recursion — Base Case, Stack Depth & Tail Recursion

recursion.cpp — factorial, Fibonacci recursive vs iterative, tail recursionC++
#include <iostream>
using namespace std;

// ── Factorial — classic recursion ──────────────────────────────
long long factorial(int n) {
    if (n <= 1) return 1;          // ① base case — STOP condition
    return n * factorial(n - 1);   // ② recursive case — smaller input
}

// ── Fibonacci — recursive (exponential) ───────────────────────
long long fibRec(int n) {
    if (n <= 1) return n;
    return fibRec(n-1) + fibRec(n-2);  // O(2^n) — slow for large n!
}

// ── Fibonacci — iterative (linear) ────────────────────────────
long long fibIter(int n) {
    if (n <= 1) return n;
    long long prev = 0, curr = 1;
    for (int i = 2; i <= n; i++) {
        long long next = prev + curr;
        prev = curr; curr = next;
    }
    return curr;
}

// ── Tail recursion — accumulator pattern ──────────────────────
long long factTail(int n, long long acc = 1) {
    if (n <= 1) return acc;      // base: return accumulated result
    return factTail(n-1, n*acc);  // tail call — GCC optimizes to loop
}

int main() {
    cout << factorial(10)         << endl;  // 3628800
    cout << factTail(10)          << endl;  // 3628800
    cout << fibRec(10)             << endl;  // 55
    cout << fibIter(10)            << endl;  // 55
    cout << fibIter(50)            << endl;  // 12586269025 (fast)
    return 0;
}
Output
3628800
3628800
55
55
12586269025
Recursion golden rules: (1) Always have a base case. (2) Every recursive call must bring the input closer to the base case. (3) Know your stack depth — most systems limit the stack to 1–8 MB (about 10K–100K frames). (4) For large n, prefer iterative or memoized versions over naive recursion.
🏆 Section 8

DSA Examples: Sort, Binary Search, GCD, Merge Sort

bubble_sort.cpp — swap by referenceC++
#include <iostream>
#include <vector>
using namespace std;

void swap(int& a, int& b) noexcept {
    int tmp = a; a = b; b = tmp;
}

void bubbleSort(vector<int>& arr) {   // pass by ref — modify in place
    int n = arr.size();
    for (int i=0; i<n-1; i++) {
        bool swapped = false;
        for (int j=0; j<n-1-i; j++) {
            if (arr[j] > arr[j+1]) { swap(arr[j], arr[j+1]); swapped=true; }
        }
        if (!swapped) break;  // already sorted — early exit
    }
}

int main() {
    vector<int> v = {64, 34, 25, 12, 22};
    bubbleSort(v);
    for (int x : v) cout << x << " ";
}
Output
12 22 25 34 64
binary_search.cpp — iterative + recursiveC++
#include <iostream>
#include <vector>
using namespace std;

int binarySearch(const vector<int>& arr, int target) {
    int lo=0, hi=(int)arr.size()-1;
    while (lo <= hi) {
        int mid = lo + (hi-lo)/2;  // avoids overflow vs (lo+hi)/2
        if (arr[mid] == target) return mid;
        if (arr[mid] < target)  lo = mid+1;
        else                    hi = mid-1;
    }
    return -1;
}

int main() {
    vector<int> arr = {1,3,5,7,9,11};
    cout << binarySearch(arr, 7)  << endl;  // index 3
    cout << binarySearch(arr, 6)  << endl;  // -1 not found
}
Output
3
-1
gcd_lcm.cpp — Euclidean algorithm, recursive + constexprC++
#include <iostream>
using namespace std;

// Euclidean GCD — compile-time when called with constants
constexpr int gcd(int a, int b) noexcept {
    return b == 0 ? a : gcd(b, a % b);
}

constexpr long long lcm(int a, int b) noexcept {
    return (long long)a / gcd(a, b) * b;
}

int main() {
    constexpr int g = gcd(48, 18);  // compile-time!
    cout << "GCD(48,18)= " << g << endl;    // 6
    cout << "LCM(4,6)=  " << lcm(4,6) << endl; // 12
    cout << "GCD(100,75)=" << gcd(100,75) << endl; // 25
}
Output
GCD(48,18)= 6
LCM(4,6)= 12
GCD(100,75)=25
merge_sort.cpp — divide-and-conquer recursionC++
#include <iostream>
#include <vector>
using namespace std;

void merge(vector<int>& arr, int l, int m, int r) {
    vector<int> left(arr.begin()+l, arr.begin()+m+1);
    vector<int> right(arr.begin()+m+1, arr.begin()+r+1);
    int i=0, j=0, k=l;
    while (i<(int)left.size() && j<(int)right.size())
        arr[k++] = (left[i]<=right[j]) ? left[i++] : right[j++];
    while (i<(int)left.size())  arr[k++]=left[i++];
    while (j<(int)right.size()) arr[k++]=right[j++];
}

void mergeSort(vector<int>& arr, int l, int r) {
    if (l >= r) return;        // base case: 1 element
    int m = l + (r-l)/2;
    mergeSort(arr, l, m);       // sort left half
    mergeSort(arr, m+1, r);     // sort right half
    merge(arr, l, m, r);        // merge both halves
}

int main() {
    vector<int> v = {38,27,43,3,9,82,10};
    mergeSort(v, 0, v.size()-1);
    for (int x : v) cout << x << " ";
}
Output
3 9 10 27 38 43 82

Best Practices & Common Mistakes

✅ Single responsibility

One function, one task. If you can't describe it in one sentence, it's doing too much — split it.

✅ const ref for large input params

void f(const vector<int>& v) — no copy, read-only. Never pass large objects by value in production code.

✅ Return values, not output params

int compute() is more testable and composable than void compute(int& result). Use structured bindings for multiple returns.

✅ Mark noexcept and const

int size() const noexcept. Documents intent, enables optimizations, and enables std::vector move.

✅ Use [[nodiscard]] on error-prone returns

Error codes, checksums, allocation results. Prevents silent ignoring of critical return values.

✅ Prefer lambdas for short inline logic

Use named functions for anything longer than 5 lines or called from more than one place.

❌ Missing base case in recursion

Infinite recursion → stack overflow → crash. Always identify and implement the base case first.

❌ Returning reference to local variable

int& f() { int x=5; return x; } — dangling reference, undefined behavior. Return by value or by reference to a member/static.

FAQ / Interview Questions

Pass-by-value creates a copy — the function cannot modify the caller's variable. Pass-by-reference gives the function an alias to the original variable — any modification affects the caller. Use pass-by-value for small types (int, double) where copying is cheap. Use pass-by-const-reference for large types (string, vector) where you want efficient read-only access. Use pass-by-reference (non-const) when the function must modify the caller's variable (output parameters, swap).
Function overloading allows multiple functions to share the same name as long as their parameter lists differ (in type, count, or order). The compiler selects the correct version at compile time based on argument types. Rules: (1) Return type alone cannot distinguish overloads — it must be parameter differences. (2) Default arguments can create ambiguity. (3) const-qualification on reference parameters distinguishes overloads. The compiler rejects if it cannot uniquely determine which overload to call.
A lambda is an anonymous inline function: [capture](params){ body }. Use lambdas for: (1) Arguments to STL algorithms (sort comparators, find predicates). (2) Callbacks used in one place — no need to create a named function. (3) Capturing local state that a free function cannot access. Use named functions when: the logic is longer than 5 lines, it's called from more than one place, or you want to test it independently. Prefer auto for storing lambdas locally — it's zero overhead versus std::function which has type-erasure cost.
Function templates generate a function for each type used — the compiler writes the specializations for you: template<typename T> T max(T a, T b) works for any comparable type. Overloading requires writing each version manually with different parameter types. Use templates when the algorithm is identical for all types. Use overloading when the logic differs per type (e.g., string concatenation vs numeric addition). Templates can be combined with overloading — you can provide a specific overload for a type that needs different behavior.
Default arguments: (1) Must be specified right-to-left — you cannot have a default for a parameter unless all parameters to its right also have defaults. (2) Should be in the declaration (header file), not the definition — otherwise callers in other files don't see them. (3) Can cause ambiguity with overloaded functions — the compiler may not be able to pick one. (4) Cannot use local variables as defaults — only constants, global variables, or expressions evaluable at compile time.
Four approaches: (1) Output parameters (pass by reference): clearest for one or two outputs. (2) std::pair<T,U> or std::tuple<T,U,V> with C++17 structured bindings: auto [min, max] = minMax(v);. (3) Named struct: most readable for 3+ values — field names document their meaning. (4) std::optional<T> for a value that may not exist. Avoid returning multiple values through global variables — makes functions non-reentrant and hard to test.

Related C++ Topics on CoodeVerse

Variables & Types Arrays & Strings Classes & Objects Templates Exception Handling Debugging & Optimization STL & Algorithms 📚 Full C++ Course

CoodeVerse Editorial Team

Senior C++ engineers and CS educators. All code tested with GCC 13 and Clang 16, -Wall -Wextra -std=c++17.