Skip to content

Lambdas: When and How to Use Them

amirroth edited this page Oct 2, 2021 · 4 revisions

Functions have been "first-class citizens" in C++ since the C days. In a computer language, a "first class citizen" refers to any entity that can be assigned to a variable, passed to a function as an argument, and returned from a functions as value. You can do all of these things with C/C++ functions. This is how callbacks and virtual functions are implemented among other things. As an aside, an example of something that isn't a "first-class citizen" in C/C++ is types. You cannot assign int to a variable in C, or pass a the name of a class as an argument to a function. There are some languages, e.g., python, in which you can do these things, and a lot of cool capabilities emerge from that. In C/C++ you cannot, which partially motivates the need for templates.

Anyways, being able to pass a function as a parameter to another function is useful capability. The classic C/C++ example is the sort which can take a custom comparison function as an argument. Here is an example where an vector of structs is sorted using a custom comparison function that compares structure name values.

struct S_c {
   std::string name;
   int data;
};  

bool sCompare(struct S_c const &s1, struct S_c const &s2) { return strcmp(s1.name, s2.name) < 0; }

std::vector<struct S_c> sVec;

void sSort()
{
   ... 
   std::sort(sVec.begin(), 
             sVec.end(), 
             sCompare);
   ...
}

Cool. What about this lambda stuff?

Lambdas

In C++11, the language added the lambda construct. At first blush, the lambda construct appears to be just a cute way of defining a single-use function directly at the point of use.

void sSort()
{
   ...
   std::sort(sVec.begin(), 
             sVec.end(), 
             [] (struct S_c const &s1, struct S_c const &s2) -> bool { return strcmp(s1.name, s2.name) < 0; }); 
}

Like all good first-class citizens, lambdas can also be assigned to variables and thus named.

void sSort()
{
   ...
   auto sCompare = [] (struct S_c const &s1, struct S_c const &s2) -> bool 
      { return strcmp(s1.name, s2.name) < 0; };
  
   std::sort(sVec.begin(), 
             sVec.end(), 
             sCompare); 
   ...
}

Cute, right? But what fundamental new capability does lambda add? Well, nothing in this case. But this is a degenerate case, in which the body of sCompare function contains no "free variables". A "free variable" is one that is not passed to the function as a parameter. Consider a slightly different case, where you want to sort the list of structs in either a case-sensitive or case-insensitive way depending on a flag. How could you pass this flag to the comparison function? Let's try this without a lambda first.

bool sCompare2(struct S_c const &s1, struct S_c const &s2, bool case_insensitive) {
   return (case_insensitive ? strcmpi(s1.name, s2.name) : strcmp(s1.name, s2.name)) < 0);
}  
      
void sSort2(bool case_insensitive)
{
   ...
   std::sort(sVec.begin(),
             sVec.end(),
             sCompare2,
             case_insensitive);
   ...
} 

This is not going to work. First, sCompare2 has the wrong signature for a comparison function template. A comparison function only takes two arguments, not three. Second, std::sort itself only takes three arguments, not four. There is no way to pass the case_insensitive argument to sCompare2 as its third argument. This, ladies and gentlemen, is where the lambda construct flexes.

Closures

A lambda is more than a just-in-time function definition, it is what is called a "lexical closure" or just "closure" for short. A closure is a function definition plus the binding of all free variables in that function from the enclosing (i.e., defining) scope.

You see that [] at the beginning of the lambda function definition? Those brackets contain the names of free variables in the lambda function that are to be "bound" or "captured" from the enclosing scope. By placing a variable in that list, you are "binding" or "capturing" it and allowing it to be used in the body of the lambda without passing it as a parameter. We can now do this with case_insensitive. Let's go straight to the example.

void sSort2(bool case_insensitive)
{
   ...
   auto sCompare2 = [case_insensitive] (struct S_c const &s1, struct S_c const &s2) -> bool   
      { return (case_insensitive ? strcmpi(s1.name, s2.name) : strcmp(s1.name, s2.name)) < 0); };
  
   std::sort(sVec.begin(), 
             sVec.end(), 
             sCompare2); 
   ...
}

[case_insensitive] binds the value of case_insensitive from the surrounding context (i.e., the sSort2 function) and allows it to be used in the body of sCompare2. This works even though the std::sort function that sCompare2 is being passed to does not have the case_sensitive variable in its scope. 🤯 This bit of magic is the "next-level" power of the lambda construct and what you cannot do with normal functions.

Lambda constructs are not needed very often, but when they are needed they are magical. Trying to fake this type of functionality in some other way gets nasty and complicated in a hurry.

Clone this wiki locally