If you are an experienced C++ programmer, this is undeniably the way to write a function:

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

C++ is a statically typed language, and you can in fact type a function. Like how qsort function wants comp function for compare, you can write:

typedef int (*binary_fun)(int, int);
int do_math(binary_fun fun, int a, int b) {
    return fun(a, b);
}
int main() {
    return do_math(add, 1, 2);      // return 3
}

But note that in C and C++, you can cast to a type. Which means I can cast an ASCII string into a function:

// Non-portable. x86-64 only
#define EXECPERM __attribute__((section(".text")))
#ifdef __linux__ 
char const add[] EXECPERM = "\x8d\x04\x37\xc3";
#elif _WIN32
char const add[] EXECPERM = "\x8d\x04\x0a\xc3";
#endif
int main() {
    do_math((binary_fun)add, 1, 2); // return 3
}

or even from an array into a function:

// Non-portable. x86-64 Little-endian only
#ifdef __linux__ 
long const add[] EXECPERM = {0xc337048d};
#elif _WIN32
long const add[] EXECPERM = {0xc30a048d};
#endif
int main() {
    do_math((binary_fun)add, 1, 2); // return 3
}

And it should function like an add function given the variable is in an executable region (else you will be stopped by NX bit or similar). This is extremely useful if you want to embed one whole function in assembly and do not want to deal with GNU as, MASM, etc. I used this in one of my challenges on MinutemanCTF Training Platform as you could see here: dungwinux:minuteman24-rev:inception/inception.c

On the other hand, say, you want instead a function that holds and preserves the variables. Tranditionally, this can be done through static variables.

int get_id() {
    static int counter = 0;
    return counter++;
}
int main() {
    get_id();
    get_id();
    return get_id();    // return 2
}

Thanks to operator overloading, we can construct an object that behave like a function and can preserve variables:

struct GetId {
    int counter = 0;
    int operator()() {
        return counter++;
    }
};
auto get_id = GetId{};
int main() {
    get_id();
    get_id();
    return get_id();    // return 2
}

You may notice that the closure of the “function” is much narrower. The static variable counter is preserved across program (any call inside program will change the same variable), while the struct property counter is preserved across the object lifetime (calls to two different instances of GetId will change different counter). This is also different from lambda, since it owns counter rather than inheriting counter of the parent (lambda-capturing). You can find this pattern widely used in the std::ranges namespace implementation (for example, std::ranges::join_view in MSVC STL, LLVM libc++, GCC libstdc++). So don’t limit yourself to preserving variables, since you can do a lot more when it is in fact a struct.