Chapter 3 Exploring Functions
3.1 Defaulted and deleted functions
=default;
can be applied to special class member functions while=delete;
can be applied to any function.c++1
2
3
4template <typename T>
void run(T val) = delete;
void run(long val) {} // can only be called with long integersAbout the rules of auto generation of special class member functions, see Item 17, Effective Modern C++.
3.2 Using lambdas with standard algorithms
Essentially lambdas are syntactic sugar of unnamed function objects, whose copy and move constructor and destructor are defaulted and assign operators are deleted.
Note that lambda cannot (but I had a try, it seems not the case?) capture variables with static storage duration (i.e. variables defined in namespace scope or with
static
specifier).And also, lambda cannot capture
this
by reference.Lambdas have several kinds of specifier such as
mutable
,constexpr
and so on.
3.3 Using generic lambdas
Generic lambdas have
auto
as its parameters, which is essentially a function object with a template asoperator()
:c++1
2
3
4
5
6
7
8
9
10
11
12
13[](auto const s, auto const n) { return s + n; };
// is syntactic sugar to
struct __lambda_name__
{
template<typename T1, typename T2>
auto operator()(T1 const s, T2 const n) const { return s + n; }
__lambda_name__(const __lambda_name__&) = default;
__lambda_name__(__lambda_name__&&) = default;
__lambda_name__& operator=(const __lambda_name__&) = delete;
~__lambda_name__() = default;
};
3.4 Writing a recursive lambda
Lambdas can also be recursive (even though it’s rarely used):
c++1
2
3
4std::function<int(int const)> lfib = [&lfib](int const n) {
return n <= 2 ? 1 : lfib(n - 1) + lfib(n - 2);
};
auto f10 = lfib(10);There are some points to mention since the lambda itself is captured by reference:
std::function
needs to be used here instead ofauto
because it’s required that compiler knows the type of the lambda when capturing it;- the lambda itself cannot be captured by value, because at the time of capture, the lambda is an incomplete type yet, whose
operator()
will throw astd::bad_function_call
when invoked.
3.5 Writing a function template with a variable number of arguments
Before C++11, we can only write some functions accepting a variable number of arguments with variadic macros (something related to
va_list
,va_begin
,va_end
, etc.) and there is no way to create classes with variable number of members.While after C++11, both can be done with variadic templates.
One thing worth noting is that we can use
std::cout << __PRETTY_FUNCTION__ << std::endl
to get the signature of a function with its substituted template parameters:c++1
2
3
4
5template<typename T>
void f(T t) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
// void f(T) [with T = int]
}
3.6 Using fold expressions to simplify variadic function templates
Fold expressions, which is introduced since C++17, essentially applies a binary function to a range of values to produce a single result. Basically it has four forms:
(... op pack)
(init op ... op pack)
(pack op ...)
(pack op ... op init)
If the ellipses appears in the left of
pack
, it means a left-folding (like((1+2)+3)
) and withinit
, it’s allowed to provide an initial value.
3.7 Implementing higher-order functions map and fold
A higher function is one that takes other functions as parameters and applies them to a range of values. Some common examples of higher-order functions include map and fold.
Map is to say apply a transform function to a range and produce a new range of data, which can be implemented with
std::transform()
.Fold is to say apply a combining function to a range and produce a single result, which can be implemented with
std::accumulate()
or variadic templates.
3.8 Composing functions into a higher-order function
A fancy application of variadic template is to create composed functions:
c++1
2
3
4
5
6
7
8
9template <typename F, typename G>
auto compose(F&& f, G&& g) {
return [=](auto x) { return f(g(x)); };
}
template <typename F, typename... R>
auto compose(F&& f, R&&... r) {
return [=](auto x) { return f(compose(r...)(x)); };
}Now we could use
compose()
to compose functions into a single one:c++1
2
3
4
5
6auto n = compose(
[](int const n) {return std::to_string(n); },
[](int const n) {return n * n; },
[](int const n) {return n + n; },
[](int const n) {return std::abs(n); }
)(-3); // n = "36"And we can even overload the
operator*
to write likef * g
instead ofcompose(f, g)
.
3.9 Uniformly invoking anything callable
- Callable has many forms such as function pointer, functor and lambda. It’s convenient for library writers to invoke them in a uniformed way, and
std::invoke()
comes in C++17. - Actually,
std::invoke()
can not only invoke a callable, but also get the value of a data member. To be more precise, supposestd::invoke()
has form ofstd::invoke(f, arg1, arg2...)
,- if
f
is a pointer to a member function, thenarg1
is treated likethis
pointer; - if
f
is a pointer to a data member, then there should be only a singlearg1
(no moreargN
) and it’s treated likethis
, whosef
member is the return value; - if
f
is other callable, then this call tostd::invoke()
is equivalent tof(arg1, arg2...)
- if