C++ Functions: Syntax, Overloading, Lambdas, Templates & Recursion — Complete Guide (2025)
⚡ Quick Answer: Functions in C++
- Function — named reusable block: return_type name(params) { body }
- Pass-by-value — copy made; pass-by-ref (&) — modify original; const ref — read-only efficient
- Overloading — same name, different parameter types/count; resolved at compile time
- Default arguments — must be right-to-left; declared in header
- Lambda —
[capture](params){ body }; inline anonymous function - template<typename T> — generic function for any type
- constexpr — computed at compile time when args are constants
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
Function Anatomy — Declaration vs Definition
#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
}
8
Hello, Alice!.hpp headers, definitions go in .cpp files. Within a single file, put the definition before the first call — then no prototype is needed.
Parameter Passing — All 5 Modes Compared
void f(int x)Use for: small types (int, double, bool)
void f(int& x)Use for: output parameters
void f(const string& s)Use for: large input-only params
void f(int* x)Use for: optional params, C APIs
void f(vector<int>&& v)Use for: factory functions, sinks
#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;
}
5
99
len=11
99
Received 3 elements
0int, 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.
Function Overloading — Rules & Pitfalls
#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;
}
8
8
Hello World
6
24| Factor | Distinguishes overloads? | Example |
|---|---|---|
| Parameter types | ✓ Yes | f(int) vs f(double) |
| Parameter count | ✓ Yes | f(int) vs f(int, int) |
| Parameter order | ✓ Yes | f(int, double) vs f(double, int) |
| Return type only | ✗ No — compile error | int f() vs double f() |
| const qualifier on ref | ✓ Yes | f(int&) vs f(const int&) |
Default Arguments
#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;
}
[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)Lambda Functions & std::function
#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;
}
25
12
13
9 8 5 3 2 1
Count > 4: 3
8
15Function Templates & constexpr
#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;
}
20
3.14
Z
7.5
3628800
49
532Recursion — Base Case, Stack Depth & Tail Recursion
#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;
}
3628800
3628800
55
55
12586269025DSA Examples: Sort, Binary Search, GCD, Merge Sort
#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 << " ";
}
12 22 25 34 64 #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
}
3
-1#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
}
GCD(48,18)= 6
LCM(4,6)= 12
GCD(100,75)=25#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 << " ";
}
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
[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.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.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.Master C++ functions and write production-ready code
Structured lessons, 200+ exercises, completion certificate. Join 50,000+ students on CoodeVerse.