Introduction to Function Objects
A function object is one unit that holds both the value and the operation. The operation receives an element from the algorithm and uses the carried value to decide the answer. Nothing outside the object needs to remember what the value was.
A Counting Problem That Needs a Custom Question
Suppose you have a list of words and you want to count how many of them come before the word "m" in alphabetical order. A general counting algorithm can walk through every element of the list, but it does not know your rule. For each element it needs to ask a question, and the question you need is not "is this element equal to some value?" It is "is this element less than a particular string?"
The central idea of this article is that the comparison belongs to a small object you pass in, rather than being written into the algorithm itself. A function object can supply that question to a general algorithm, and seeing how it does so is the goal of the next several sections.
What do you think happens?
Given the words apple, kiwi, mango, and zebra, how many come before m in alphabetical order?
Reveal answer
2
Apple and kiwi sort before m. Mango does not, because m is a prefix of mango and therefore sorts before it. Zebra sorts after m.
Tracing an Object That Holds Its Own Value
A function object is an object that can be used as if it were a function. The notation Less_than<string>{s} describes an object that compares to the string called s. The important detail is that the object holds that string inside itself. When you create it, the value it compares against travels with it. Calling the object with an element asks whether that element is less than the stored string.
The following example is an original illustration built from this idea. It defines a class template that carries a value and compares against it, then passes an object of that class to the standard count_if algorithm. Read the code once before looking at the output, and notice that the algorithm itself never mentions the letter m.
2- The class template stores one value of any type T inside the object.
operator()makes the object callable, so it can be used where a function is expected.- Less_than<std::string>{"m"} creates an object that carries the string m with it.
count_ifasks the object about each element and counts the answers that are true.
Tracing count_if element by element
Count how many elements of apple, kiwi, mango, and zebra make the Less_than object holding m return true.
- 1Element apple
The algorithm passes apple to the object. The object checks whether apple is less than m. It is, so the count becomes 1.
- 2Element kiwi
The object checks whether kiwi is less than m. It is, so the count becomes 2.
- 3Element mango
The object checks whether mango is less than m. It is not, because m is a prefix of mango and sorts before it. The count stays at 2.
- 4Element zebra
The object checks whether zebra is less than m. It is not, so the count stays at 2.
The count is 2. The algorithm never compared anything to m directly; the carried value did that work.
Why Carrying the Value Beats Separate Functions
Without function objects, a separate ordinary function would be needed for each value you might want to compare against. Each value type might need its own function as well. The alternative would be to keep the value in a global variable that the function reads. Both approaches make the code harder to follow, because the knowledge needed to understand one comparison is spread across several places.
Function objects avoid both problems. You do not write a separate function for each value and type, and you do not introduce global variables to hold the values. The comparison and the value it uses are packaged together in one unit, which you create wherever you need it.
A function object is one unit that holds both the value and the operation. The operation receives an element from the algorithm and uses the carried value to decide the answer. Nothing outside the object needs to remember what the value was.
Why Simple Function Objects Are Cheap to Call
Carrying data is only half of the story. For a simple function object such as Less_than, inlining is straightforward. Inlining means the body of the operation can be placed directly where it is used, instead of being reached through a separate function call at run time. Because of this, a call to Less_than is far more efficient than an indirect function call.
The two properties support each other. Carrying data makes the object convenient, and its efficiency makes it practical. Together they explain why function objects are especially useful as arguments to algorithms, which may call the argument many times over a sequence of elements. In the count_if example, the comparison runs once for every element in the list, so a cheap call matters.
The Policy Idea: Same Algorithm, Different Rule
The word policy describes the role of the object. The algorithm is general: it knows how to walk a sequence and count matches. The policy object decides what counts as a match. In this sense a policy object is a function object that defines the meaning of a key operation inside a general algorithm such as count().
Changing the policy changes the meaning of the operation without changing the algorithm. The count_if call in the earlier example could be given a different policy for a different question, such as an object that tests for words longer than a given length, and the algorithm would be reused unchanged.
Writing the Object Where It Is Used
The example above has a drawback. You have to define Less_than separately from its use, and that separation feels inconvenient when the object is needed for only one call. To address this, the language provides a notation for implicitly generating function objects. Its motivation is to let you describe the object at the point where it is needed instead of writing a separate definition first.
2- The lambda expression on the third argument generates a function object at the point of use.
- The comparison rule is written right where the algorithm receives it.
- The call produces the same result as the named Less_than version.
Function Adaptors: Input and Output
Sometimes a function needs several pieces of information to do its work, but the code that wants to use it only has some of those pieces when it is ready to call. One option is to rewrite the function. Another is to wrap it: keep the original function as it is, and produce a new callable that already holds some of the information and asks only for the rest. Function adaptors are the tools built around that second idea.
A function adaptor takes a function as an argument and returns a function object that can be used to invoke the original function.
Notice the direction of the relationship. An ordinary function takes data in and gives a result back. An adaptor takes a function in and gives a different callable object back. The adaptor does not perform the work the original function was written to do. Its job is to package the original function into a new form that a caller can use more conveniently. The adaptor reshapes how the function is called without changing what the function does.
Argument Binding as Partial Evaluation
The technique that gives adaptors their practical value is called argument binding. The same idea goes by two other names: Currying and partial evaluation. All three names point to one act: fixing some of a function's arguments ahead of time, so that the resulting object needs fewer arguments when it is finally called.
The word partial is the useful clue. The function is not evaluated in full at the point of binding. Only part of its input is supplied. The remaining input is left open, and the returned object waits for it. When the missing inputs arrive and the object is invoked, the original function receives the complete set it expects.
Binding the first argument of a subtraction
A function f takes two arguments and returns a minus b. Bind a to 10, then call the resulting object with b equal to 3.
- 1Start with the original
f(a, b) needs two inputs before it can produce a result.
- 2Bind a to 10
The binder stores 10 as the first argument. The result is a new callable g(b) that needs only one input.
- 3Invoke g with 3
Calling g(3) passes 10 and 3 to the original function, which computes 10 minus 3.
g(3) returns 7. The original f is unchanged and still needs both arguments.
Binders in the Standard Library and Lambdas
The standard library provides two adaptors for argument binding: bind() and mem_fn(). Each takes a function-like thing and returns an object that can be used to invoke it. When you meet them in code, recognize them as adaptors that perform the partial evaluation described above.
Binders were heavily used in the past, but most of those uses are now more easily expressed with lambdas. A lambda is an alternative to an adaptor for argument binding. In the cases the material describes, the same effect can be written more directly as a lambda, so an adaptor is no longer the first tool most programmers reach for.
Common Mistakes When Reading Function Objects
Common mistake Assuming the algorithm knows the comparison rule
What it looks likeExpecting count_if to know that you want words before m without being given any rule.
Why it failsA general algorithm walks the sequence and asks a question about each element, but the question comes from the object you pass in.
Do this insteadLook at the function object argument to find the rule the algorithm is applying.
Common mistake Thinking the value is stored in a global variable
What it looks likeLooking for a global holding m when the Less_than object was created with m.
Why it failsThe value travels inside the function object, which is why function objects avoid global variables.
Do this insteadFind the value in the object's constructor argument or braced initializer.
Common mistake Believing the lambda form changes what the object does
What it looks likeThinking a lambda written inline behaves differently from a named Less_than object with the same comparison.
Why it failsThe implicit notation changes where the definition is written, not how the object behaves once it exists.
Do this insteadCompare the operation each version performs, not the place where it is written.
Common mistake Expecting a binder to perform the original work
What it looks likeThinking bind() itself computes the subtraction when it is only given the fixed argument.
Why it failsAn adaptor takes a function in and returns a different callable; the original function still does the work when the returned object is invoked.
Do this insteadTrace the call forward: the caller invokes the returned object, which forwards to the original function.
Common mistake Confusing partial evaluation with full evaluation of the function at the binding point
Practice: Trace a Binding
A function g takes two arguments and returns x minus y. You bind x to 20, producing a new callable h that takes only y. What does h return when it is called with y equal to 8? Then explain which argument was fixed and which one the caller still supplies.
Hints
- Binding fixes the first argument of g, so x is always 20 inside h.
- The caller supplies y, which is the single argument h still needs.
- Compute 20 minus 8 once you know both values.
Check your answer against the worked example earlier in the article. The structure is the same: one argument is fixed in advance, the returned object needs fewer arguments, and invoking it sends the complete set to the original function. The answer is 12.
Key Takeaways
- A function object carries its comparison value inside itself, so you do not need a separate function for each value or a global variable to hold it.
- Simple function objects such as Less_than can be inlined, which makes calling them far more efficient than an indirect function call when they are passed to algorithms.
- A policy object is a function object that defines the meaning of a key operation in a general algorithm, so changing the policy changes behavior without changing the algorithm.
- A function adaptor takes a function in and returns a function object that invokes the original; argument binding, also called Currying or partial evaluation, fixes some arguments in advance.
- The standard library's
bind()andmem_fn()perform argument binding, but lambdas are now often preferred for the same job.