-
Notifications
You must be signed in to change notification settings - Fork 3
Callable Objects: Part 3
####Exercise 1: All lambda functions may be directly translated into a functor. While lambda functions offer a more concise syntax, that same syntax may be harder to understand than the more verbose functor. The first exercise hopes to connect the two.
Examine the following lambda:
[&d, i](double lhs, int rhs) -> double
{
d += i;
return lhs + rhs + d;
}It references a double from the scope where the lambda is declared, and copies an int from the same scope.
The following struct shows a possible translation:
struct match_lambda
{
match_lambda(double& d, int i) : d(d), i(i) {}
double operator()(double lhs, int rhs)
{
d += i;
return lhs + rhs + d;
}
double& d;
const int i;
};Because d was captured by reference, it was modifiable. Why can't you modify i? How would you change the lambda to allow it?
Note that match_lambda::i is of type const int by default. This means that not only will trying to modify i not change the variable in the outer scope, attempts to modify it inside the lambda won't compile.
[i](int val) { i += val; } //ERROR: Cannot modify i.This was done to avoid surprising the unwary programmer who might think that modifying a variable captured from an outer scope should always result in changing that variable. If you really want to modify variables captured by value, you may declare the lambda mutable:
[i](int val) mutable { i += val; } //OKAY: This still won't change i in the outer scope, though.This makes the following change to match_lambda above:
double& d;
- const int i;
+ int i;
};A bonus example -- note the returned value of f versus i:
int i = 3;
auto f = [=]() mutable -> int
{
i += 2;
return i;
};
std::cout << i << std::endl; // 3
std::cout << f() << std::endl; // 5
std::cout << f() << std::endl; // 7
std::cout << i << std::endl; // 3####Exercise 2/Question 2:
You can create and use a lambda function at local scope. Since the type of a lambda is unspeakable, we can either use auto to deduce the lambda type or std::function to erase its type. Which of the following would be faster?
auto af = [](int i) { return i * 2; };
std::function<int(int)> sf = [](int i) { return i * 2; };####Answer 2:
In my timings, the auto-stored lambda is approximately 3x faster than the std::function-stored lambda. There is an extra level of indirection involved when using std::function. The cost is reasonable and the uses of std::function many. By and large there's no reason to use std::function for lambdas used only in the local scope [^1] -- it's more useful for the cases we'll discuss later.
####Exercise 3/Question 3:
If a lambda (or any callable object, really) is being passed to another function, what is the speed difference between having that function be templatized and having that function accept a std::function?
[](int i) { return std::sqrt(static_cast<double>(i)); }####Answer 3: The speed difference is more significant. In my tests, the templatized function ran nearly 11x faster. This is why standard algorithms like accumulate and transform are templatized on the function:
template <typename InputIterator, typename OutputType, typename Function>
OutputType accumulate(InputIterator begin, InputIterator end, OutputType initial, Function f);There are, however, good reasons to use std::function when passing a callable object. For one, it's not possible[^2] to instantiate a templatized function whose declaration is in a header file and whose definition is in a separate cpp file. So using std::function can reduce dependencies. Furthermore, a function which accepts std::function isn't instantiated (at least) once for every different argument -- there is only one definition. This may impact compile and link times. Also, std::function clearly indicates the expected form of the callable object to execute, which template types (currently) do not.
Another reason to use std::function may be seen in part 4 below.
####Exercise 4/Question 4:
std::function<void(int)> return_callable() { return [](int i) { std::cout << i << std::endl; }; }Given that std::function and other type-erased methods are the only generic ways to return a callable object, how would you go about returning such an object if genericity weren't required and std::function were unacceptable?
####Answer 4:
You could obviously just return the callable entity itself, assuming it isn't a C++ lambda function. Therefore, functors may be returned by name, functions may be returned by function pointer. Whether you use std::function or not, be sure not to reference variables which will go out of scope in the object you're returning!
####Exercise 5: Note the following objects which must be notified of ongoing key presses:
class character_displayer
{
public:
void display_character(char c) const
{ std::cout << "Key pressed: " << c << std::endl; }
};
class signal_character_tester
{
public:
void indicate_if_value(char c) const
{
if(c == 'Y')
std::cout << "Y has been pressed." << std::endl;
}
};keyboard_inputter is tightly coupled to character_displayer and signal_character_tester. We expect that more objects will need to be made aware of key presses in the future, and we're unhappy with the number of changes required to keyboard_inputter:
class keyboard_inputter
{
public:
keyboard_inputter(
const character_displayer& cd, const signal_character_tester& sct)
: cd_(cd), sct_(sct)
{}
char operator()()
{
char c;
std::cin >> c;
//Notify the objects that need to hear about this.
cd_.display_character(c);
sct_.indicate_if_value(c);
return c;
}
private:
const character_displayer& cd_;
const signal_character_tester& sct_;
};
void get_input()
{
character_displayer cd;
signal_character_tester sct;
keyboard_inputter ki(cd, sct);
char c = 0;
while(c != 'N')
{
c = ki();
}
}Change keyboard_inputter, character_displayer, signal_character_tester and get_input to use std::function to decouple keyboard_inputter from knowledge of the other classes.
The changes needed are actually pretty simple:
class keyboard_inputter
{
public:
typedef std::function<void(char)> key_listener;
char operator()()
{
char c;
std::cin >> c;
//Notify the objects that need to hear about this.
std::for_each(cbs_.begin(), cbs_.end(), [c](key_listener cb) { cb(c); });
return c;
}
void add_key_listener(key_listener cb)
{ cbs_.push_back(cb); }
private:
std::vector<key_listner> cbs_;
};
void get_input()
{
character_displayer cd;
signal_character_tester sct;
keyboard_inputter ki;
ki.add_key_listener([&cd](char c) { cd.display_character(c); });
ki.add_key_listener([&sct](char c) { sct.indicate_if_value(c); });
...This gives the ability to register arbitrary handlers without modifying keyboard_inputter. This pattern[^3] is so common that several libraries support it. For a case like the previous, Boost.Signals may be used to simplify the implementation and make it more powerful. Here's a potential implementation of keyboard_inputter using it:
class keyboard_inputter
{
typedef boost::signal<void(char)> key_signal;
public:
char operator()()
{
char c;
std::cin >> c;
//Notify the objects that need to hear about this.
key_signal_(c);
return c;
}
boost::signals::connection add_key_listener(key_signal::slot_function_type cb)
{ return key_signal_.connect(cb); }
private:
key_signal key_signal_;
};In addition to a slight typing savings, it is possible to use the returned connection to temporarily or permanently disconnect a listener [^4].
Hopefully this gives you a good basis for implementing command patterns in modern C++. If you've never used the pattern, you'll likely find that it can simplify and improve your code.
[^1] One case is when trying to call a lambda recursively. Another is when you wish to choose between two lambdas:
std::function<void(int)> f;
if(...)
f = [&](...){branch 1};
else
f = [&](...){branch 2};Both cases are pretty uncommon, and you should probably do the former without lambdas anyway.
[^2] There are some limited & occasionally useful circumstances when function template declarations may be separated from their definitions. I won't go into them now, but it's mostly about reducing compile and link times.
[^3] wikipedia has a nice introduction, languages like node.js and frameworks like the C++ Rest SDK also use it extensively.