Template Basics and Generic Algorithms
Function objects can be used as arguments to algorithms, so the algorithm supplies the general process and the function object supplies the specific behavior.
Why Algorithms Need a Behavior Argument
Many generic algorithms do not know in advance exactly what they should do with each element they process. A sorting routine may need to decide which of two elements comes first, and a search routine may need to test each element against a condition. In template-based code, the algorithm is written once for many element types, so the part of its behavior that varies is usually handed to it from outside.
The two tools this article covers, function objects and lambdas, are two ways of providing that small piece of behavior at the point where the algorithm is called. Keep one practical question in mind as you read: when you call an algorithm that needs a behavior argument, should that behavior be a named object defined somewhere else, or should it be written directly inside the call? The rest of this article shows how the source resolves that question.
Tracing a Sort With a Comparison Rule
Consider a sorting algorithm that needs to know, for any two elements, which one should come first. The algorithm does not hard-code a single ordering. Instead it repeatedly asks the behavior it was given. The diagram below shows the algorithm making several such requests, each time handing two elements to the rule and receiving a yes or no answer about their order.
3 2 1 - DescendingOrder is a function object: a class whose
operator()can be called like a function - std::sort is the generic algorithm; it supplies the general process of ordering elements
- DescendingOrder{} is the specific behavior passed in as the third argument
What do you think happens?
The vector starts as 3, 1, 2 and the algorithm is given DescendingOrder. What order will the numbers be printed in?
Reveal answer
3 2 1
The comparison rule returns true when the first argument is larger, so the algorithm places larger values first. The sort algorithm itself did not change; only the behavior argument determined the direction.
Function Objects as Arguments
A function object is an object that can be called as if it were a function, and it can be passed as an argument to an algorithm. Because the algorithm only needs something it can call, the same algorithm can be reused with many different behaviors.
This is the reason function objects fit naturally with templates. The template supplies the reusable process, and each function object supplies one particular rule for that process to follow. The algorithm stays the same from one call to the next; only the argument changes.
Reusing one algorithm with two behaviors
Use the same sort algorithm to order a list in two different ways, without writing a second sort.
- 1Identify the general process
The sort algorithm decides how to arrange elements, but it needs a rule for which of two elements comes first.
- 2Provide the first behavior
Pass a function object whose call returns true when the first argument should come before the second, such as the ascending rule.
- 3Provide the second behavior
Pass a different function object, such as the descending rule, to the same algorithm. The algorithm code is unchanged.
- 4Compare the results
The two calls produce opposite orders because only the behavior argument differs.
One generic algorithm serves both orderings. Its general process was written once, and each function object supplied one specific rule.
Lambdas as Inline Function Objects
A lambda provides the same kind of thing as a named function object: a callable behavior that an algorithm can receive as an argument. What differs is where the behavior is written. A lambda is written directly inside the call, while a named function object is defined somewhere else and referred to by name.
3 2 1 - The lambda [](int a, int b) { return a > b; } is written inline as the behavior argument
- The output matches the named function object version, because the behavior is the same
Deciding Between a Named Object and a Lambda
When you write a call that needs a behavior argument, the deciding question is how many places in the program need that behavior. The source's guidance reduces every choice to this one question, so the table below compares the two forms by where the behavior lives and how many places refer to it.
| Aspect | Named function object | Lambda |
|---|---|---|
| Where the behavior is defined | Separately, as a named object | Inline, inside the call |
| Type of thing supplied to the algorithm | A callable behavior | A callable behavior |
| Best when the behavior is needed | In several places | In one place only |
| Effect on the surrounding code | Adds a named definition elsewhere | Keeps the behavior next to its single use |
Both forms supply a callable behavior; they differ in where that behavior lives and how many places refer to it.
Applying this rule to a concrete case makes the choice easy to justify. Suppose one sorting call in a program needs a comparison rule that no other code uses. A lambda keeps that rule beside the single call that needs it. Now suppose three different parts of the program sort records using the same ordering. Defining that ordering once as a named function object and passing it in each place avoids repeating the rule three times.
Mistakes When Choosing a Form
Common mistake Writing the same lambda in several places
What it looks likeCopying the same comparison lambda into three separate sort calls in different files.
Why it failsThe source's guidance is that a behavior needed in several places is better defined once. Repeated lambdas must be kept in sync by hand.
Do this insteadDefine the behavior once as a named function object and pass that object to each call.
Common mistake Assuming a function object and a lambda are different kinds of argument
What it looks likeBelieving that an algorithm must be given a named class and cannot accept an inline lambda.
Why it failsBoth forms supply a callable behavior to the algorithm; the difference is where the behavior is written.
Do this insteadTreat both as the same kind of argument and choose between them based on how many places need the behavior.
Common mistake Choosing a named function object for a one-line rule used once
What it looks likeDefining a separate struct with an operator() that only returns a single comparison, then using it in one call.
Why it failsThe extra definition adds distance between the rule and the only place it is used, and the source identifies one-place use as the lambda case.
Do this insteadWrite the simple rule as a lambda directly inside the call that needs it.
Common mistake Forgetting that the algorithm, not the call site, decides when the behavior is invoked
Practice Choosing a Form
A program sorts customer records by last name in one place only. Another function, in a different file, also needs to sort records by last name. Which form should the shared ordering use, and why? Justify your answer using the deciding question from this article.
Hints
- Ask how many places in the program need this behavior.
- Think about whether the behavior should be defined once and passed to both call sites.
Key Takeaways
- A generic algorithm supplies the general process, and a function object supplies the specific behavior passed in as an argument.
- A function object is callable like a function, so the same algorithm can be reused with many different behaviors.
- A lambda is the appropriate choice for a simple function object needed in one place only.
- The deciding question when choosing between a named function object and a lambda is how many places in the program need the behavior.
- A behavior needed in several places is better defined once as a named function object and passed wherever it is needed.