Lambda functions in C++, when to use and what are the advantages?

Asked

Viewed 1,064 times

7

When choosing to use a lambda function or a normal function, and what is the advantage of using a lambda function compared to a normal function? There is the call price of a function in a lambda function?

2 answers

3


Lambdas solve a problem of readability, expressiveness and practicality. For example: before , if you wanted to pass a predicate to some algorithm of , there are a few options:

  • Pass a previously defined function;
  • Define a functor* and pass an instance;
  • Use an existing functor in the standard library;
  • Use Boost.Lambda (library of ) to define a pseudo-lambda.

None of these options are perfect or simple enough. Let’s use an example to illustrate the problem: we need to find the first character that doesn’t match the regex [A-Za-z]* in a string. Let’s go by part, using the options from the list above (which is not exhaustive):

Pass a previously defined function:

#include <algorithm> // para std::find_if_not
#include <iterator> // para std::begin e std::end

bool is_alpha(char c)
{
    return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}

const char str[] = "abcd1234";
const char *it = std::find_if_not(std::begin(str), std::end(str), is_alpha);

Understand the function is_alpha being passed as argument to the algorithm std::find_if_not. Having to define a function for each point in the code that uses an algorithm for is not practical, as well as making the readability and practicality a little worse, since you will have to look for the definition of predicates, which may be far from the point of use. One of the side effects of this technique is that a compiler may not fully optimize, leaving its existing definition in binary and as valid as not replacing the call by the body of the function, resulting in a poorly optimized program in that particular part. This is just speculation, of course. Always measure the performance of the program before you draw any conclusions. The points about readability and practicality still remain.

Define a functor and pass an instance:

struct IsAlpha
{
    bool operator() (char c) const
    {
        return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
    }
};

const char *it = std::find_if_not(std::begin(str), std::end(str), IsAlpha{});

The functor can be defined locally, near the point of its use. However, readability is not improved, as there is now much more code to understand. In this case, an instance of struct IsAlpha is passed as predicate to the algorithm std::find_if_not. Predicate works by calling the function call operator overload operator(), that plays the role of checking the pattern for a given character. The expressiveness of this technique is also questionable (it takes a greater understanding to recognize a functor), but it is justifiable if the functor is simple enough and is reused several times in the whole code.

Use an existing functor in the standard library:

There is no functor with the same intention as our predicate is_alpha, making this solution very limited (we have some functors.) Changing example, we can use std::greater<> as functor for the algorithm std::sort:

std::vector<int> v{4, 3, 1, 5};
std::sort(begin(v), end(v), std::greater<void>());

Here, we have good readability (since std::greater is part of the standard library, so its behavior is always the same for everyone), a reasonable expressiveness (it would be better to be able to use built-in operator operator>) and it’s practical. Of course, this is an invented example; other situations may require more elaborate predicates, making this technique unusable.

See the introductory examples to the library Boost.Lambda. One of them resolves the expressiveness with the previous technique, in exchange for legibility and practicality:

std::sort(begin(v), end(v), *_1 > *_2);

What *_1 and *_2 means it is not obvious to anyone: it is necessary to search in the documentation of Boost.Lambda to understand all possible effects and what can be done with these placeholders. In this case, *_1 > *_2 creates a function object whose predicate has the same purpose as std::greater. This solution does not scale well with more elaborate predicates and can harm compilation time, as well as the quality of the optimized program.

All these alternatives fail in one of the three points: readability, expressiveness and practicality. With this in mind, German functions were introduced in in order to resolve these three points. Lambdas are merely function objects (i.e., their functioning is identical to how we defined structs for functors earlier). A explanation by Maniero is enough and I don’t need to repeat it here.

Let’s use the first example with a lambda:

auto it = std::find_if_not(std::begin(str), std::end(str), [](char c) {
    return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
});

The predicate is visible right at the point of use, which favors the readability of the code and practicality. There is no magic in the definition of the predicate; we can use any artifice of language freely in the body of lambda, favoring expressiveness. All three points are solved with this solution. Let’s see the other example:

std::sort(begin(v), end(v), [](int a, int b) { return a > b; });

Although we have more code now (compared to std::greater<>() and *_1 > *_2), We no longer need to depend on any external library, nor seek the definition of any functor, or even understand how the functor is defined. Of course, in this particular example, use std::greater is recommended in place of lambda, since such a functor is standard in the language.

Finally, there are more features in Amblas (such as variable capture, Amblas with revolving closure/closure etc) that are outside the scope of the question.

Typically, optimizing a lambda is easier than a function, since there is no external binding name (because the lambda definition is anonymous) and no function address is passed as argument, only the instance of the struct that defines the lambda. That is, it is a value that is useful to the language type system and that the compiler can generate minimal code, which makes use only of the lambda body.

A good general rule of thumb is to always use both, unless there is already a function object defined by the standard (as in the case of std::greater.)

* A functor, or Function Object (function object), is an unofficial term (being Function Object the official term) used by programmers in . One of its definitions may be an object for which the function call operator is defined.

2

To lambda does not have a name defined at compile time, it is treated as a value.

Think that the normal function is like a constant and the only way to call it is to use the name of this constant. A lambda is a function that is a value and needs to be assigned to a variable, then you call the variable, so you are running the function.

As it is a value can be assigned to several variables, anywhere in the code, including arrays and parameters.

So it gives a huge flexibility, you can make algorithms or structures that have a basis of how it should be, but some part will be defined later at some other point in the code, then the execution is parameterized.

You know inheritance and polymorphism? When allowed (whenever it is virtual), a derived class can swap the base class algorithm (although almost always the ideal is to do something extra and call the base class method).

The same goes for the interface (which does not exist in C++ yet, only a purely abstract class, but in C++20/23 you can have through library, and yes, you can create syntax through library). You have a function signature/method, but not the implementation. The implementation comes in the class, then.

To lambda allows something more flexible, each object can have a different implementation. Of course, it needs to have some place in the object that is waiting for a lambda with the same signature.

Generally the price may be the same as a normal virtual method, that is, it has a indirect additional, but nothing more. Depending on the implementation can be a little better than the virtual method, although I think any decent compiler optimizes so much that it equals the lambda.

One lambda can become a closure, there is an additional cost of memory and processing because of the captured variables.

And this is another advantage of lambda, as it can be transported to several places, the variables of scope external to it become part of it, as in an internal normal function, but the normal function does not go anywhere, so the external variables are accessible in it, but do not need to be captured.

I don’t want to go too far into this because it’s not the focus of the question.

Under certain circumstances it is possible to exchange it with a pointer to function or functor.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.