Skip to content

Functions

Functions are the building blocks of C++ codebases. They encapsulate logic, promote code reuse, and control execution paths. Modern C++ provides highly optimized ways to declare, parameterize, and execute functional logic, including inline execution and lambdas.


1. Declarations vs. Definitions

To keep source code organized, C++ splits code components into: - Declaration (Prototype): Specifies the function's interface (name, parameters, and return type). It does not contain code. Typically put in header files (.hpp or .h). - Definition: The actual implementation containing the logic. Typically written in source files (.cpp).

// math_utils.hpp
#pragma once // Prevents double inclusion

int multiply(int a, int b); // Function Declaration

// math_utils.cpp
#include "math_utils.hpp"

int multiply(int a, int b) { // Function Definition
    return a * b;
}

2. Parameter Passing Semantics

In C++, how you pass arguments to a function has massive implications for performance and safety.

       1. Pass-by-Value
       ┌──────────────────┐
       │   Copies Data    │ ◄── [Primitive types like int, double, bool]
       └──────────────────┘
       2. Pass-by-Reference
       ┌──────────────────┐
       │ Modifies Original│ ◄── [Output parameters / updating state]
       └──────────────────┘
       3. Pass-by-Const-Reference
       ┌──────────────────┐
       │  Zero-Copy/Read  │ ◄── [Large objects: std::string, std::vector]
       └──────────────────┘

1. Pass by Value

The compiler creates a copy of the argument. Modifications inside the function do not affect the original variable.

1
2
3
4
5
6
7
8
void updateValue(int val) {
    val = 100; // Changes local copy only
}

int main() {
    int num = 5;
    updateValue(num); // num remains 5
}
Best for: small types (1-8 bytes) like int, double, bool, char.

2. Pass by Reference (&)

The function parameter becomes an alias for the original argument. Modifications directly affect the caller's variable.

1
2
3
void incrementValue(int& val) {
    val++; // Modifies the original variable
}
Best for: output parameters or modifiable states.

3. Pass by Const Reference (const &)

Passes a reference to avoid copying, but the compiler prevents any modification of the parameter.

1
2
3
4
void displayInfo(const std::string& info) {
    // info = "New State"; // Error: read-only reference
    std::cout << info << "\n";
}
Best for: large types like std::string, std::vector, and custom classes/structs.

3. Function Overloading & Name Mangling

You can define multiple functions with the same name, as long as their parameter types or quantities differ. This is called Function Overloading.

1
2
3
void print(int val) { std::cout << "Integer: " << val << "\n"; }
void print(double val) { std::cout << "Double: " << val << "\n"; }
void print(const std::string& val) { std::cout << "String: " << val << "\n"; }

How Name Mangling Works

Behind the scenes, the C++ compiler distinguishes overloaded functions by generating unique names for the linker. This is called Name Mangling. For example, the function void print(int) might compile to a linker symbol like _Z5printi, while void print(double) becomes _Z5printd.


4. Lambda Expressions

Lambdas are anonymous, in-line functions. They are extremely powerful for temporary operations (e.g., passing predicates to STL algorithms).

Lambda Syntax Anatomy

[ capture_list ] ( parameters ) -> return_type { body }
  • [capture_list]: Defines what variables from the surrounding scope are accessible inside the lambda.
    • []: Empty capture. No outside variables are accessible.
    • [=]: Capture all variables in the surrounding scope by value (read-only copy).
    • [&]: Capture all variables in the surrounding scope by reference (modifiable).
    • [x, &y]: Capture x by value and y by reference.
  • mutable: By default, lambdas capturing by value are read-only. Adding mutable allows you to modify variables captured by value inside the lambda body (though it won't affect the external variable).
#include <iostream>

int main() {
    int counter = 10;

    // Lambda capturing by value. Without 'mutable', 'counter++' would fail compilation.
    auto increment = [counter]() mutable {
        counter++; 
        std::cout << "Inside lambda: " << counter << "\n"; // Prints 11
    };

    increment();
    std::cout << "Outside lambda: " << counter << "\n"; // Prints 10 (unchanged)

    return 0;
}

5. Compile-Time Functions: constexpr and consteval

constexpr Functions

A constexpr function can be evaluated at compile time if the inputs are known at compile time. If the inputs are only known at runtime, it runs as a standard runtime function.

constexpr int add_constexpr(int a, int b) {
    return a + b;
}

int main() {
    // Evaluated at compile-time (optimizes to direct constant placement)
    constexpr int val = add_constexpr(10, 20); 
    static_assert(val == 30, "Validation check");

    // Evaluated at runtime
    int x = 5;
    int y = add_constexpr(x, 10); 
}

consteval Functions (C++20)

Introduced in C++20, consteval functions (also known as immediate functions) must be evaluated at compile time. Running them with runtime parameters generates a compiler error.

consteval int force_compile_time_math(int x) {
    return x * x;
}

int main() {
    constexpr int ans = force_compile_time_math(5); // OK

    // int run_val = 5;
    // int ans2 = force_compile_time_math(run_val); 
    // ERROR: run_val is not a compile-time constant!
}

6. Trailing Return Types

Modern C++ provides two ways to specify return types: 1. Auto Deduction (C++14): The compiler deduces the return type automatically from the return statement. 2. Trailing Return Type: Useful when the return type depends on the parameters or when writing complex template functions.

1
2
3
4
5
6
7
8
9
// 1. Auto deduction
auto add(int a, int b) {
    return a + b; // Deduced as int
}

// 2. Trailing return type
auto multiply(double a, double b) -> double {
    return a * b;
}