Lesson 8: Functions
C++11 introduced some additional features related to functions - a new style syntax, lambda functions (which usually are equivalent of functors), easier ways to represent function pointers etc.
Alternative Function Syntax
C++11 introduced an alternative syntax for writing function declarations. Instead of putting the return type before the name of the function (e.g. int func()
), the new syntax allows us to write it after the parameters (e.g. auto func() -> int
).
Basically, instead of writing the return type before the name of the function, we specify auto
and specify the return type after the parameter list. Since the return type appears at the end of the declaration, the function is said to have a trailing return type. Both of the declarations above are equivalent, which means that they mean exactly the same. This new order of declaring a function is used in a number of modern programming languages, and has the benefit of more closely following the literal definition of a function.
The use of the auto
keyword here is only part of the syntax and does not perform automatic type deduction in this case. Automatic type deduction of functions was added in C++14, and would kick in if we did not provide the trailing return type.
Simplifies generic code
template<typename Lhs, typename Rhs>
auto add(const Lhs& lhs, const Rhs& rhs) -> decltype(lhs + rhs)
{
return lhs + rhs;
}
Eliminates repetition
class LongClassName
{
using IntVec = std::vector<int>;
IntVec f();
};
LongClassName::IntVec LongClassName::f()
{
// Classic definition …
}
auto LongClassName::f() -> IntVec
{
// alternative function syntax…
}
Consistency with lambda expressions
[](int i) -> double { /* ... */ };
The alternative syntax was added to aid writing of generic code and to provide consistency. However, due to the volume of existing code and familiarity of the classic syntax, the original syntax is used more widely than the new syntax. Even C++ Core Guidelines generally use the original syntax.
Function Objects (functors)
Functors are objects that can be treated as though they are a function or function pointer - you could write code similar to:
myFunctorClass functor;
functor( 1, 2, 3 );
This code works because C++ allows you to overload operator()
, the "function call"
operator. The function call operator can take any number of arguments of any types and return anything it wishes to. (It is probably the most flexible operator that can be overloaded; all the other operators have a fixed number of arguments).
While overloading operator()
is nice, the really nice feature of functors is that their lifecycle is more flexible than that of a function - you can use a a functor's constructor to embed information that is later used inside the implementation of operator()
.
This example creates a functor class with a constructor that takes an integer argument and saves it. When objects of the class are "called", it will return the result of adding the saved value and the argument to the functor:
#include <iostream>
class myFunctorClass
{
public:
myFunctorClass(int x) : x{ x } {}
int operator()(int y) { return x + y; }
private:
int x;
};
int main()
{
myFunctorClass addFive( 5 );
std::cout << addFive( 6 ) << std::endl;
return 0;
}
Functors are not interchangeable with function pointers, and hence are not usable with C libraries.
Lambda Functions
One of the most exciting features of C++11 is ability to create lambda functions (sometimes referred to as closures). A lambda function is a function that you write inline in your source code (usually to pass in to another function, similar to the idea of a functor or function pointer). With lambda, creating quick functions has become much easier, eliminating in a lot of cases the requirement to write a functor.
#include <iostream>
int main()
{
auto func = []() { std::cout << "Hello World!” << std::endl; };
func(); // now call the function
}
Brackets ([]
) serve as the identifier, called the capture specification, tells the compiler we're creating a lambda function. You will see this (or a variant) at the start of every lambda function.
Similar to regular functions, there is an argument list in parentheses: ()
. IIf the compiler can deduce the return value of the lambda function, it will do so rather than force you to declare it. In this case, the compiler knows the function does not return a value. Next we have the body, printing out "Hello World!”. This line of code does not actually cause anything to print out though - we are just creating the function here. It is like defining a normal function - it just happens to be inline with the rest of the code.
It's only on the next line that we call the lambda function: func()
- it looks like any other function invocation.
Variable Capture
Lambda functions can use a variable declared outside of the lambda, and use it inside of the lambda. The compiler does need to be instructed to perform the variable capture. This is achieved by adding an ampersand for the capture specification ([&]
). An empty []
tells the compiler not to capture any variables, whereas the [&]
specification tells the compiler to perform variable capture.
#include <iostream>
#include <string>
int main()
{
const std::string text{ "Hello World!" };
[&]() { std::cout << text << std::endl; }();
return 0;
}
One of the biggest beneficiaries of lambda functions are, power users of the standard template library algorithms package.
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
using ivec = std::vector<int>;
ivec v;
v.push_back( 1 );
v.push_back( 2 );
// Pre-C++11 for loop
for ( ivec::const_iterator itr = v.cbegin(), end = v.cend(); itr != end; ++itr ) std::cout << *itr << " ";
std::cout << std::endl;
// C++11 and above
std::for_each( v.begin(), v.end(), [](int val) { std::cout << val << " "; } );
std::cout << std::endl;
return 0;
}
The parameter list (in parentheses), like the return value is also optional if you want a function that takes zero arguments.
#include <iostream>
int main()
{
[] {} // A lambda that takes no arguments and does nothing
[] { std::cout << "Hello World!”; }();
}
Return Values
By default, a lambda that does not have a return statement defaults to void
. If you have a simple return expression, the compiler will deduce the type of the return value:
```c++ { return 1; } // compiler knows this returns an integer ````
If you write a more complicated lambda function, with more than one return value, you should specify the return type. Lambdas take advantage of the new C++11 alternative return value syntax of putting the return value after the function. In fact, you must do this if you want to specify the return type of a lambda. Here's a more explicit version of the really simple example from above:
[] () -> int { return 1; } // now we're telling the compiler what we want
Implementation
Lambdas are usually implemented by creating a class that overloads the operator()
, so that it acts just like a function. A lambda function is an instance of this class; when the class is constructed, any variables in the surrounding environment are passed into the constructor of the lambda function class and saved as member variables. This is, in fact how similar feature was implemented pre-C++11 using functors. The benefit of C++11 is that doing this becomes almost trivially easy - so you can use it all the time, rather than only in very rare circumstances where writing a whole new class makes sense.
Similar to function parameters, there is flexibility in how variables are captured - all controlled via the capture specification []
. If you make a lambda with an empty capture group ([]
), rather than creating the class, C++ will create a regular function. Here's the full list of options:
[]
- Capture nothing[&]
- Capture any referenced variable by reference[=]
- Capture any referenced variable by making a copy[=, &foo]
- Capture any referenced variable by making a copy, but capture variable foo by reference[bar]
- Capture bar by making a copy; do not copy anything else[this]
- Capture the this pointer of the enclosing class
Notice the last capture option--you don't need to include it if you're already specifying a default capture (=
or &
), but the fact that you can capture the this
pointer of a function is important, because it means that you don't need to make a distinction between local variables and instance variables of a class when writing lambda functions. You can get access to both. The elegant part is that you do not need to explicitly use the this
pointer; it's really like you are writing a function inline.
Type of Lambda
The main reason for creating a lambda function is that you wish to use a function that expects to receive a lambda function. Each lambda function is implemented by creating a separate class, even single lambda function is a different type - even if the two functions have the same arguments and the same return value! C++11 does include a convenient wrapper for storing any kind of function - lambda function, functor, or function pointer: std::function
.
std::function
The std::function
template is the most convenient way of passing around lambda functions both as parameters and as return values. It allows you to specify the exact types for the argument list and the return value in the template.
#include <functional>
#include <string>
#include <vector>
class AddressBook
{
public:
std::vector<std::string> findMatchingAddresses(std::function<bool (const std::string&)> func)
{
std::vector<std::string> results;
for ( auto itr = addresses.cbegin(), end = addresses.cend(); itr != end; ++itr )
{
// call the function passed into findMatchingAddresses and see if it matches
if ( func( *itr ) ) results.push_back( *itr );
}
return results;
}
private:
std::vector<std::string> addresses;
};
The parameter expected by findMatchingAddresses
is any function/functor/lambda that returns a bool
and accepts as parameter a const reference
to a string.
References
- http://www.bfilipek.com/2016/12/please-declare-your-variables-as-const.html
- http://www.bfilipek.com/2016/11/iife-for-complex-initialization.html
- http://www.modernescpp.com/index.php/functional-in-c-dispatch-table
- https://blog.petrzemek.net/2017/01/17/pros-and-cons-of-alternative-function-syntax-in-cpp/