C++ Core Guidelines: Pass Function Objects as Operations

Contents[Show]

An interface is a contract between a user and an implementer and should, therefore, be written with great care. This holds also true if you pass an operation as argument.

 contract

Today, I'm just writing about rule 40 because function objects are used quite heavily in modern C++.                   

T.40: Use function objects to pass operations to algorithms

First of all, you may be irritated that the rules didn't explicitly mention lambda functions but use them. I write about this point later in detail.

There are various ways to sort a vector of strings.

 

// functionObjects.cpp

#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>

bool lessLength(const std::string& f, const std::string& s){      // (6) 
    return f.length() < s.length();
}

class GreaterLength{                                              // (7)
    public:
        bool operator()(const std::string& f, const std::string& s) const{
            return f.length() > s.length();
    }
};

int main(){

    std::vector<std::string> myStrVec = {"523345", "4336893456", "7234", 
	                                     "564", "199", "433", "2435345"};

    std::cout << "\n";                                      
    std::cout << "ascending with function object" << std::endl;  
    std::sort(myStrVec.begin(), myStrVec.end(), std::less<std::string>()); // (1)
    for (const auto& str: myStrVec) std::cout << str << " ";  
    std::cout << "\n\n";
  
    std::cout << "descending with function object" << std::endl;             
    std::sort(myStrVec.begin(), myStrVec.end(), std::greater<>());         // (2)
    for (const auto& str: myStrVec) std::cout << str << " ";  
    std::cout << "\n\n";

    std::cout << "ascending by length with function" << std::endl;
    std::sort(myStrVec.begin(), myStrVec.end(), lessLength);               // (3)
    for (const auto& str: myStrVec) std::cout << str << " ";  
    std::cout << "\n\n";

    std::cout << "descending by length with function object" << std::endl;
    std::sort(myStrVec.begin(), myStrVec.end(), GreaterLength());          // (4)
    for (const auto& str: myStrVec) std::cout << str << " ";  
    std::cout << "\n\n";

    std::cout << "ascending by length with lambda function" << std::endl;
    std::sort(myStrVec.begin(), myStrVec.end(),                           // (5)
              [](const std::string& f, const std::string& s){ 
		          return f.length() < s.length(); 
			  });
    for (const auto& str: myStrVec) std::cout << str << " ";  
    std::cout << "\n\n";

}

The program sorts a vector of strings lexicographically and based on the length of the strings. I used in the lines (1) and (2) two function objects from the Standard template library. A function object is an instance of a class for which the call operator (operater ()) is overloaded. Often, there are falsely called functors. I hope, you notice the difference between the call std::sort(myStrVec.begin(), myStrVec.end(), std::less<std::string>()) in line (1) and std::sort(myStrVec.begin(), myStrVec.end(), std::greater<>()) n line (2). The second expression (std::greater<>()), in which I provided no type for the predicate, is valid since C++14. I sorted in the lines (3), (4), and (5) by using a function (6), a function object (7), and a lambda function (5). This time, the length of the strings was the sorting criteria.

For completeness, here is the output of the program.

functionObjects

The rules states you should "Use function objects to pass operations to algorithms".

Advantages of function objects

My argumentation boils down to three points: Performance, Expressiveness, and State. It makes my answer quite easy that lambda functions are function objects under the hood.

Performance

The more the optimiser can reason locally, the more optimisation is possible. A function object (4) or a lambda function (5) can be generated just in place. Compare this to a function that was defined in a different translation unit. If you don't believe me, use the compiler explorer and compare the assembler instructions. Of course, compile with maximum optimisation.

Expressiveness

"Explicit is better than implicit". This meta-rule from Python also applies to C++. It means that your code should explicitly express its intent. Of course, this holds in particular for lambda functions such in line (5). Compare this with the function lessLength in line (6) which is used in line (3). Imagine your coworker would name the function foo; therefore, you have not idea what the function should do. You have to document their usage such as in the following line.

// sorts the vector ascending, based on the length of its strings 
std::sort(myStrVec.begin(), myStrVec.end(), foo); 

 

Further, you have to hope that your coworker wrote a correct predicate. If you don't believe him you have to look at the implementation. Maybe that's not possible because you just have the declaration of the function. With a lambda function, your coworker can not fool you. The code is the truth. Let me put it more provocative: Your code should be such expressive that it needs no documentation. 

State

In contrast to a function, a function object can have state. The code example makes my point.

// sumMe.cpp

#include <algorithm>
#include <iostream>
#include <vector>

class SumMe{
  int sum{0};
  public:
    SumMe() = default;

    void operator()(int x){
      sum += x;
    }

    int getSum(){
      return sum;
    }
};

int main(){

    std::vector<int> intVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 

    SumMe sumMe= std::for_each(intVec.begin(), intVec.end(), SumMe());  // (1)

    std::cout << "\n";
    std::cout << "Sum of intVec= " << sumMe.getSum() << std::endl;      // (2)
    std::cout << "\n";

}

 

The std::for_each call in line (1) is crucial. std::for_each is a special algorithm of the Standard Template Library because it can return its callable. I invoke std::for_each with the  function object SumMe and can, therefore, store the result of the function call directly in the function object. I ask in line (2) for the sum of all calls which is the state of the function object.

sumUp

 

Just to be complete. Lambda functions can also have state. You can use a lambda function to accumulate the values.

// sumMeLambda.cpp

#include <algorithm>
#include <iostream>
#include <vector>

int main(){
	
	std::cout << std::endl;

    std::vector<int> intVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    std::for_each(intVec.begin(), intVec.end(),
	            [sum = 0](int i) mutable {
				    sum += i; 
				    std::cout << sum << std::endl;
				});
    
    std::cout << "\n";

}

 

Okay, this lambda function looks scary. First of all, the variable sum represents the state of the lambda function. With C++14, the so-called initialisation capture of lambdas is supported. sum = 0 declares and initialises a variable of type int which is only valid in the scope of the lambda function. Lambda functions are per default const. By declaring it as mutable, I can add the numbers to sum.

sumUpLambda

 

I stated that lambda functions are functions objects under the hood. C++ Insight makes the proof for my statement to a piece of cake.

Lambda Functions are Function Objects

A lambda function is just syntactic sugar for a function object which is instantiated in place. C++ Insight shows which transformations the compiler applies to lambda functions.

Let's start simple. When I run the following small lambda function in C++ Insight

sourceLambda

the tool gives me the unsugared syntactic sugar:

insightLambda

The compiler generates a function object __lamda_2_16 (lines 4 - 11), instantiates it in line 13, and uses it line 14. That's all!

The next example is a little bit more complicated. Now, the lambda function addTo adds the sum to the variable c which is captured by copy.

sourceLambdaCapture

 

In this case, the autogenerated function object gets a member c and a constructor. This is the code from C++ Insight.

insightLambdaCapture

What's next?

This was just the first rule to templates interfaces. My next post continues their story.

 

 

Thanks a lot to my Patreon Supporters: Eric Pederson, Paul Baxter,  Meeting C++, Matt Braun, Avi Lachmish, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Dilettant, Marko, and Ramesh Jangama.

Thanks in particular to:  TakeUpCode 450 60

 

Get your e-book at Leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 600 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Get your interactive course

 

Modern C++ Concurrency in Practice

C++ Standard Library including C++14 & C++17

educative CLibrary

Based on my book "Concurrency with Modern C++" educative.io created an interactive course.

What's Inside?

  • 140 lessons
  • 110 code playgrounds => Runs in the browser
  • 78 code snippets
  • 55 illustrations

Based on my book "The C++ Standard Library" educative.io created an interactive course.

What's Inside?

  • 149 lessons
  • 111 code playgrounds => Runs in the browser
  • 164 code snippets
  • 25 illustrations

Add comment


Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 2345

All 1233278

Currently are 195 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments