Local functions in standard C++ (kind of...)
1. Introduction
TL;DR: Local functions are not allowed in C++, but static public member functions of local classes are.
We occasionally need to define little auxiliary functions that are only used in a specific code segment, perhaps only once. A textbook example occurs when we use the C qsort
function, which requires a pointer to a separate comparison function. A standard code example looks like this:
#include <stdio.h>
#include <stdlib.h>
static int compare_ints(const void* a, const void* b) {
int int_a = *(const int*)a;
int int_b = *(const int*)b;
return (int_a > int_b) - (int_a < int_b);
}
int main() {
int arr[] = {3, 6, 8, 10, 1, 2, 1};
size_t n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(int), compare_ints);
for (size_t i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
That we need to introduce the auxiliary function at the global scope of the translation unit is somewhat unfortunate. In the above example, we have added static
to prevent the function from being externally visible, that is, to other translation units, but it nevertheless remains visible at the file scope, even though such functions are typically only used in some specific situations.
Can we avoid this namespace pollution?
Some programming languages, such as JavaScript, allow the definition of local function within code segments. In other words, these languages permit the nested definition of functions. This is not available in standard C but exists as a language extension in some compilers. For example, consider the following code segment
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[] = {3, 6, 8, 10, 1, 2, 1};
int compare_ints(const void* a, const void* b) { // ❌ Not allowed in standard C or C++
int int_a = *(const int*)a;
int int_b = *(const int*)b;
return (int_a > int_b) - (int_a < int_b);
}
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), compare_ints);
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
While GCC permits such local functions as a language extension, the above has never been permissible standard C. The historical rationale might have been lost to history by now. Perhaps it is a relic of the time when all local variables had to be declared at the beginning of functions, and adding local functions was deemed too complicated, or perhaps the presence of local functions would complicate compiler design, since C famously prides itself on being implementable on rather tiny compilers. However, the existence of such language extensions shows that there has been persistent interest in this concept.
We will not focus too much on C here. Instead, we will discuss how C++ facilitates de-facto local functions in a standard conforming manner with almost no syntactic overhead. Importantly, these de-facto local functions have one advantage over lambdas: they can be used where function pointers are expected.
2. Local Type Definitions in C and C++
A lesser-known feature of both C and C++ is that these languages allow for local type definitions. This applies to typedef
s, enum
s, and struct
s in C.
The following is perfectly valid C:
#include <stdio.h>
void example() {
typedef unsigned int uint;
struct Point {
int x, y;
};
enum Color { RED, GREEN, BLUE };
struct Point p = {1, 2};
printf("Point: (%d, %d)\n", p.x, p.y);
}
int main() {
example();
return 0;
}
The same is true for C++, where we can easily define classes in the local scope of our functions. For example, the following is permitted in C++:
#include <iostream>
void example() {
class Point {
public:
int x, y;
void print() { std::cout << "Point: (" << x << ", " << y << ")\n"; }
};
Point p = {1, 2};
p.print();
}
int main() {
example();
return 0;
}
- Static Member Functions in Local Classes
Such local classes in C++ have only some minor restrictions in comparison to classes defined within namespaces. The most significant one seems to be that they cannot have static data members. That restriction seems tolerable because we can replace static data members by local static variables with minimal notational effort.
The key feature, though, is the possibility of static member functions. For example:
#include <iostream>
void example() {
struct sayHelloClass {
static void sayHello() {
std::cout << "Hello from a static member function!\n";
}
};
sayHelloClass::sayHello();
}
int main() {
example();
return 0;
}
For all intents and purposes, the static member function of the local class satisfies the same functionality as a local function would.
Using a Local Struct for qsort
in C
Coming back to our original problem of providing a function pointer to the C standard qsort
function without an auxiliary function clogging the namespace of the translation unit, we find the following solution:
#include <stdio.h>
#include <stdlib.h>
void sort_array(int* arr, size_t size) {
struct Comparator {
static int compare(const void* a, const void* b) {
int int_a = *(const int*)a;
int int_b = *(const int*)b;
return (int_a > int_b) - (int_a < int_b);
}
};
qsort(arr, size, sizeof(int), Comparator::compare);
}
int main() {
int arr[] = {3, 6, 8, 10, 1, 2, 1};
size_t n = sizeof(arr) / sizeof(arr[0]);
sort_array(arr, n);
for (size_t i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
Here, Comparator::compare
is assigned to the function pointer argument of C's qsort
function. There is no global namespace pollution because the static member function is only visible where the local class is visible: within the function sort_array
.
The above example shows how local functions can effectively be defined within C++ without the use of lambdas, the most important application being the interface with C functions that expect function pointers. The only syntactical overhead is the definition of a local auxiliary class.
Using a Local Struct for std::sort
in C++
For the sake of the comparison, let us have a look at the C++ standard sort function. This function, too, accepts a function pointer for the comparison function:
#include <iostream>
#include <vector>
#include <algorithm>
void sort_vector(std::vector<int>& arr) {
struct Comparator {
static bool compare(int a, int b) {
return a < b;
}
};
std::sort(arr.begin(), arr.end(), Comparator::compare);
}
int main() {
std::vector<int> arr = {3, 6, 8, 10, 1, 2, 1};
sort_vector(arr);
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
Alternatively, we can use a lambda with little effort:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> arr = {3, 6, 8, 10, 1, 2, 1};
std::sort(arr.begin(), arr.end(), [](int a, int b) {
return a < b;
});
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
While this last example seems to be the modern C++ way of solving the problem, at least according to the enthusiasts of modern C++, lambdas cannot be used as function pointers, which forecloses the possibility of using them as auxiliary functions that interface with C libraries.
Local functions in C or C++ ... ever?
Will C++ ever introduce true local functions? This seems to be unlikely for several reasons. C has left out local functions for decades and probably will continue to do in order to keep the language simple. C++ will probably not take such a big step that alienates it from C at the core foundations of the language. Moreover, if one stays within the C++ ecosystem, then lambdas seem to be a perfectly workable alternative.
The likely applications of the static-member-function-of-local-class trick are:
- Interfacing with C libraries that expect function pointers, where lambdas are not permitted
- Very unlikely but possible: highly performance-critical sections with numerous small auxiliary functions, where the overhead of lambdas is too much
The main point is that even without language extensions, we have a standard way of defining local auxiliary functions within the code.