A lambda expression with an empty capture clause is convertible to a function pointer. It can replace a stand-alone or static member function as a callback function pointer argument to C API.
Quite often, C++ applications depend on third-party or operating system libraries that are entirely written in C or expose C-only interface. Some examples of these C-APIs are database drivers, messaging service APIs, and native threading interfaces. Usually, these C-APIs take function pointer parameters for callback arguments. Let's take the example of pthread_create from the POSIX pthread library.
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
The pthread_create function creates a new thread that starts execution by invoking the parameter, start_routine. The last parameter, arg, is passed as the argument to start_routine.
Suppose, we are creating a Worker class that periodically does some work in a background thread. The Worker's start method creates a new thread through pthread_create. The new background thread always executes in a loop in the Worker's run method until the loop is terminated. The stop method sets a boolean flag to end the run loop and joins the thread. Below is the code of the Worker class:
// requires pthread.h and unistd.h
class Worker {
public:
// Implemented later
int start();
void stop() {
stopped_ = true;
// wait for the thread to exit by joining
pthread_join(threadId_, nullptr);
}
private:
void run() {
while(!stopped_) {
std::cout << "Running\n";
usleep(1000000); // sleep 1 second
}
std::cout << "Stopped\n";
}
pthread_t threadId_;
bool stopped_{false};
};
When it comes to calling pthread_create in Worker::start, there are three choices to pass as start_routine parameter: a stand-alone function in the file scope, a static member function of the Worker, or a capture-less lambda expression. Note that, we cannot pass a nonstatic member function of Worker as start_routine because the nonstatic member functions have implied hidden this
parameter. The stand-alone function and static member function choices require us to write a separate function, which is tedious and hinders the readability. Besides, the stand-alone function solution is an anti-OOP pattern. That brings us to the implementation of Worker::start using capture-less lambda expression:
int Worker::start() {
return
pthread_create(&threadId_,nullptr,
[](void* self) -> void* { // capture-less lambda
static_cast<Worker*>(self)->run();
return nullptr;
},
this); // 'this' as argument to lambda
}
The lambda expression in the above code has an empty capture clause ([ ]), takes a void*
parameter, and has an explicit return type of void*
to match the start_routine type. It casts the self parameter to Worker* and calls the run method on it. The last parameter to pthread_create is the this
pointer, which is subsequently passed to the lambda expression as an argument. We can use the Worker class in the main function, as shown below:
int main() {
Worker w;
if(w.start() == 0) {
usleep(10000000); // wait 10 seconds
w.stop();
} else {
std::cout << "Error\n";
}
}
As expected, this code prints "Running" few times followed by "Stopped" before ending.
It is shown how a capture-less lambda could be passed as a function pointer to a C API routine. How about a capturing lambda, can that be passed as a function pointer? The answer is, no. The following variation of Worker::start implementation with a capturing lambda fails to compile:
int Worker::start() {
return
pthread_create(&threadId_,nullptr, // ERROR
[this](void*) -> void* { //lambda captures 'this'
run();
return nullptr;
},
nullptr);
}
This begs the question, what makes capture-less lambda expressions so different that they can be passed as function pointers? The answer lies in the details of how lambda expressions are implemented. Conceptually, a lambda expression is syntactic sugar on a function object (an instance of a class with an overloaded function-call operator '()'). According to cppreference:
The lambda expression is a prvalue expression of unique unnamed non-union non-aggregate class type, known as closure type, which is declared (for the purposes of ADL) in the smallest block scope, class scope, or namespace scope that contains the lambda expression.
For the sake of simplicity, think of a lambda expression as an instance of its class type known as closure type, which has an overloaded function-call operator. The variables in the lambda's capture-list are stored in the nonstatic data members of the closure type. Let's take an example of a capturing lambda expression:
int x,y;
auto lambda = [x,y](int z) { /*body*/ };
The closure type of the above lambda can be conceptualized as:
struct Closure {
//....constructors
void operator()(int z) const { /* body */ }
int x,y;
};
The function-call operator is const
unless the lambda is declared mutable
. A capture-less lambda also has a similar closure type except that there are no nonstatic data members in it. However, a capture-less lambda has an additional function-pointer conversion operator declared in it. It is that function-pointer conversion operator that makes a capture-less lambda convertible to a function pointer. The cppreference describes it as:
This user-defined conversion function is only defined if the capture list of the lambda-expression is empty. It is a public, constexpr, (since C++17) non-virtual, non-explicit, const noexcept (since C++14) member function of the closure object.
Therefore, the closure type of a capture-less lambda that takes only one void*
parameter and returns void*
could be imagined as:
struct CaptureLessClosure {
//....constructors
// function-call operator
void* operator()(void* arg) const {
return impl(arg);
}
// create a function pointer alias
using F = void* (*)(void*);
// conversion operator to the function pointer
constexpr operator F() const { return impl; }
// implementation of lambda body in a static method
static void* impl(void* arg) {
/* body (including return) */
}
};
So, why only a capture-less closure type has a function pointer conversion operator? Note that, the body of the lambda is moved to a static member function and the static function's address is returned from the conversion operator. That is one of the possible implementations of the capture-less closure type. I have only made a reasonable assumption based on the fact that because a capture-less closure type has no nonstatic data members, the body of the lambda can be implemented as a static member function, which makes it possible to convert the capture-less lambda to a function pointer.
In short, capture-less lambda expressions are convertible to function pointers and can be used as function pointer arguments in place of stand-alone or static member functions.
For more on lambda expressions: