C++ Lambdas Are Just Syntactic Sugar for Functors

Demystifying the correlation between lambda expressions and functors

What the functor?

If you’re anything like me, you’ve probably heard about functors, and maybe even can describe what they are. You’ve almost certainly used lambda expressions in C++. Perhaps you’ve even heard that lambda expressions are functors. Did you know that under the hood, lambdas are just anonymous functors?

LinkedInTable of Contents

Functor basics link icon

Understanding functors (aka: function objects) is pretty straightforward. Perhaps you’ve heard it mentioned that they’re “callable objects”. Turns out, it’s that simple! A functor is a C++ object for which the function call operator is defined.

\Basic functor which doubles a number

class FunctorDoubler {
public:
    int operator()(int x) { return x * 2; }
};

void main() {
    auto f = FunctorDoubler{ };
    std::cout << f(2) << "\n";  // 4
}

Equivalent lambda expression link icon

What makes functors interesting is that they enable lambda functions in the C++ language. When you define a local, anonymous lambda function in C++, the compiler generates a functor in its place, giving it an anonymous name under the hood.

\An equivalent lambda to the FunctorDoubler

void main() {
    auto f = [](int x){ return x * 2; };
    std::cout << f(2) << "\n";  // 4
}

Functors have state (lambda capture) link icon

Something which functors have over ordinary functions is that like any C++ object, they can have data members. This feature lets them do things like carry the context (state) of a specific object instance along with them when they are used as lambdas. This state can be persisted between calls of the lambda. In fact, this is exactly what happens when you “capture” in a lambda expression. The captured data becomes a data member of the resultant functor.

\Stateful functor

class FunctorAccumulator {
public:
    int operator()(int x) {
        acc += x;
        return acc;
    }
private:
    int acc{ };
};

void main() {
    auto accumulator = FunctorAccumulator{ };
    // the accumulated value persists between calls
    std::cout << accumulator(3) << "\n";  // 3
    std::cout << accumulator(3) << "\n";  // 6
    std::cout << accumulator(3) << "\n";  // 9
}

\Equivalent lambda expression

void main() {
    int acc{ }; // <- becomes a data member of the lambda once captured
    auto lambdaAccumulator = [&acc](int x) {
        acc += x;
        return acc;
    };

    std::cout << lambdaAccumulator(3) << "\n";  // 3
    std::cout << lambdaAccumulator(3) << "\n";  // 6
    std::cout << lambdaAccumulator(3) << "\n";  // 9
}

Compiler does the heavy lifting link icon

There’s more to it of course. The compiler does a lot of the heavy lifting, handling the finer details and edge cases for us, but, this gives us the general idea of how lambdas correlate directly to functors in C++.

Comments