Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lambdas are just classes with operator() overloaded?

The more I read about lambdas the more I hear from people that they are just function objects/functors in disguise (unless they don't capture anything, in which case they're just free static functions. I want to write lambdas at the local scope and pass them to a universal event handler, which calls them as needed, and I'm starting to notice that I can hardly do anything that a traditional function object lets me do. Please let me know if my understanding of this is wrong, as I've commented a whole bunch of stuff you can do with functors and can't with lambdas, as far as I know:

#include <iostream>
#include <vector>

struct MyFunctorClass
{
    // Custom constructor, can't do with lambda
    MyFunctorClass(int& capturedVariable) : capturedVariable(capturedVariable) 
        { std::cout << "I can do anything on construction.\n"; }

    // Overloading constructors, different ways to initialise function object, can't do with lambda
    MyFunctorClass(int& capturedVariable, int sizeOfBuffer) : capturedVariable(capturedVariable) 
        { heapAllocation = new int[sizeOfBuffer]; }

    // Custom destructor, can't do with lambda
    ~MyFunctorClass() { delete[] heapAllocation; }  

    void operator()() { std::cout << "Standard call\n"; }
    void operator()(int arg) { std::cout << "Argument passed: " << arg << '\n'; }   
    // operator() overloading, different ways to call the function object, can't do with lambda

    int* heapAllocation;                // Have heap allocated resources, can't do with lambda
    bool internalStateVariable = true;  // Initialise a member variable on construction, can't do with lambda
    int& capturedVariable;              // I can access this variable directly with MyFunctorClass::capturedVariable = 7, can't do with lambda
};

int main()
{
    int localVar = 0;
    bool trueOrFalse = false;

    {
        MyFunctorClass* myFunctionObj = new MyFunctorClass(localVar, 100);  
        // Can dynamically allocate function object, can't with lambda
        auto lambda = new[&]() { localVar = 1; };   // Can't do?
        lambda.trueOrFalse = true;      // trueOrFalse isn't member of lambda, even though it captured it, doesn't make sense

    }   // Lambda object is destroyed here. My function object lives until I delete it.

    return 0;
}

void holdFunctionObject(MyFunctorClass* funcObj)
{
    static std::vector<MyFunctorClass*> list;
    list.push_back(funcObj);    
    // I can hold all the function objects forever, they'll never go out of scope unless I delete them, can't do with lambda
}

I feel really restricted, it seems like lambdas are just a way of declaring functions "in place". They also hold state, but only can hold state of objects that are already in scope, not create new ones. And also can't be initialised in any specific ways that functors can. Have I got this right? Because they seem VERY different from just a class with overloaded operator();

like image 846
Zebrafish Avatar asked Aug 23 '17 05:08

Zebrafish


People also ask

Can lambda functions be overloaded?

No, you can not overload the lambda! Which is not possible, as the same variable name can not be reused in C++.

How do lambdas work C++?

A lambda can introduce new variables in its body (in C++14), and it can also access, or capture, variables from the surrounding scope. A lambda begins with the capture clause. It specifies which variables are captured, and whether the capture is by value or by reference.

How are lambdas stored?

The Lambda service stores your function code in an internal S3 bucket that's private to your account. Each AWS account is allocated 75 GB of storage in each Region. Code storage includes the total storage used by both Lambda functions and layers.

Why are lambdas useful C++?

It is a convenient way to define an anonymous function object or functor. It is convenient because we can define it locally where we want to call it or pass it to a function as an argument. Lambda is easy to read too because we can keep everything in the same place.


2 Answers

I don't get what's your point; yes, lambdas are just syntactic sugar for instances of classes with operator() that follow some specific template1; yes, you cannot do most of the stuff you can do with fully custom classes - and for a good reason: if you want to do that stuff, you can already write a regular class.

Lambdas take a particular, widely employed pattern (throwaway functors that may capture state)2 and gives them extremely convenient syntax3. If you need fully fledged classes, the language already have you covered since the mid 70s with the class keyword.

Also, it's important to note that, although lambdas in C++ are implemented in terms of classes, the "view of the world" that underlies the two concepts is different; closures are rooted in functional programming, not in OOP. The idea behind them is to model a single function (=action) along with the data it needs to work that was captured at creation site; an object instead is first and foremost a mutable entity which provides an interface to mutate or query its state. The fact that closures can be implemented in terms of objects and objects can be implemented in terms of closures is interesting and intriguing (and ultimately comes from the fact that both are a combination of state and code that act on it), but they start from quite different grounds. Using one or the other is essentially a matter of understanding whether what you want is primarily code that happens to capture state (a "verb") or is mostly "packaged state" with an interface to mutate it (a "noun").


  1. This is not just a shortcut to understand their implementation, they are actually specified in these terms in the standard.
  2. Pattern that comes from languages where functions and closures were first-class objects from the beginning, thus the resulting syntax sugar helps to write code in style mutated from such languages.
  3. Which, let me tell you, is a blessing; having had to port C++11 code to C++03, de-lambdifying code is an exercise in frustration - everything becomes so tediously verbose, and the result "flows" way worse when read. Capturing state is particularly horrible, generally you have to re-state the same thing at least four times - one in the declaration of the field at class level, one in the constructor prototype, one in the initializer list and one in the constructor call. Compare with the lambda capture list.
like image 190
Matteo Italia Avatar answered Oct 04 '22 03:10

Matteo Italia


unless they don't capture anything, in which case they're just free static functions

This is factually incorrect. Capture-less lambdas can be converted into function pointers, but they're not the same thing as a "free static function". They're still objects, while static functions are not (though you can have pointers to functions, that doesn't make functions be objects in the C++ sense of the term).

As for the rest, you seem to not recognize the difference between "all lambdas are functors" and "all functors are lambdas". The former is true, the latter is not, nor is the latter intended to be true.

Lambdas hold their state privately. Hand-written functors hold their state however you want. Most people would consider a functor with publicly accessible state to be bad form, but if that's what you want, that's a decision that's up to you.

By using a lambda, you agree to accept its limitations. The type of a lambda is synthesized at the point of the declaration, and it is not named, so you can't create APIs that accept a specific lambda (well, maybe you can, but you shouldn't). If you want the user to pass a specific functor type, then you should not be using a lambda.

But really... how often do you need the user to pass a specific functor type? Most of the time, what you need is for the user to pass something that can be called with a specific signature. That's what std::function is for. Alternatively, if it is a template function, you can take an arbitrary object that is callable with a specific signature.

And if you really do need the user to pass a specific functor type... so be it. But since most interfaces will not be so restricted, a lambda is an effective solution.

Lambdas are, at their core, syntactic sugar that makes it easy to write common functors. There will certainly be occasions where a lambda just cannot do what you need. But since lambdas cover about 90% of uses for such things, that makes them good enough.

The main purpose of lambdas is to logically put the code for some process adjacent to the place where you're going to invoke that process. This provides locality of the code, relative to what that process is. This is common for callbacks, continuations, and the like.

It also allows such code to access the local scope of the callback easily, compared to a hand-written functor. This makes such processes feel like part of the code, rather than having to track down the definition of the functor type and read its operator() implementation to figure out what's going on.


Also:

auto lambda = new[&]() { localVar = 1; };   // Can't do?

You can do this:

auto lambda = new auto([&]() { localVar = 1; });   // Can't do?
like image 40
Nicol Bolas Avatar answered Oct 04 '22 04:10

Nicol Bolas