In this article by Marius Bancila, author of the book Modern C++ Programming Cookbook covers the following recipes:
(For more resources related to this topic, see here.)
In C++, classes have special members (constructors, destructor and operators) that may be either implemented by default by the compiler or supplied by the developer. However, the rules for what can be default implemented are a bit complicated and can lead to problems. On the other hand, developers sometimes want to prevent objects to be copied, moved or constructed in a particular way. That is possible by implementing different tricks using these special members. The C++11 standard has simplified many of these by allowing functions to be deleted or defaulted in the manner we will see below.
For this recipe, you need to know what special member functions are, and what copyable and moveable means.
Use the following syntax to specify how functions should be handled:
struct foo
{
foo() = default;
};
struct foo
{
foo(foo const &) = delete;
};
void func(int) = delete;
Use defaulted and deleted functions to achieve various design goals such as the following examples:
class foo_not_copiable
{
public:
foo_not_copiable() = default;
foo_not_copiable(foo_not_copiable const &) = delete;
foo_not_copiable& operator=(foo_not_copiable const&) = delete;
};
class data_wrapper
{
Data* data;
public:
data_wrapper(Data* d = nullptr) : data(d) {}
~data_wrapper() { delete data; }
data_wrapper(data_wrapper const&) = delete;
data_wrapper& operator=(data_wrapper const &) = delete;
data_wrapper(data_wrapper&& o) :data(std::move(o.data))
{
o.data = nullptr;
}
data_wrapper& operator=(data_wrapper&& o)
{
if (this != &o)
{
delete data;
data = std::move(o.data);
o.data = nullptr;
}
return *this;
}
};
template <typename T>
void run(T val) = delete;
void run(long val) {} // can only be called with long integers
A class has several special members that can be implemented by default by the compiler. These are the default constructor, copy constructor, move constructor, copy assignment, move assignment and destructor. If you don't implement them, then the compiler does it, so that instances of a class can be created, moved, copied and destructed. However, if you explicitly provide one or more, then the compiler will not generate the others according to the following rules:
Sometimes developers need to provide empty implementations of these special members or hide them in order to prevent the instances of the class to be constructed in a specific manner. A typical example is a class that is not supposed to be copyable. The classical pattern for this is to provide a default constructor and hide the copy constructor and copy assignment operators. While this works, the explicitly defined default constructor makes the class to no longer be considered trivial and therefore a POD type (that can be constructed with reinterpret_cast). The modern alternative to this is using deleted function as shown in the previous section.
When the compiler encounters the =default in the definition of a function it will provide the default implementation. The rules for special member functions mentioned earlier still apply. Functions can be declared =default outside the body of a class if and only if they are inlined.
class foo
{
public:
foo() = default;
inline foo& operator=(foo const &);
};
inline foo& foo::operator=(foo const &) = default;
When the compiler encounters the =delete in the definition of a function it will prevent the calling of the function. However, the function is still considered during overload resolution and only if the deleted function is the best match the compiler generates an error. For example, giving the previously defined overloads for function run() only calls with long integers are possible. Calls with arguments of any other type, including int, for which an automatic type promotion to long exists, would determine a deleted overload to be considered the best match and therefore the compiler will generate an error:
run(42); // error, matches a deleted overload
run(42L); // OK, long integer arguments are allowed
Note that previously declared functions cannot be deleted, as the =delete definition must be the first declaration in a translation unit:
void forward_declared_function();
// ...
void forward_declared_function() = delete; // error
One of the most important modern features of C++ is lambda expressions, also referred as lambda functions or simply lambdas. Lambda expressions enable us to define anonymous function objects that can capture variables in the scope and be invoked or passed as arguments to functions. Lambdas are useful for many purposes and in this recipe, we will see how to use them with standard algorithms.
In this recipe, we discuss standard algorithms that take an argument that is a function or predicate that is applied to the elements it iterates through. You need to know what unary and binary functions are, and what are predicates and comparison functions. You also need to be familiar with function objects because lambda expressions are syntactic sugar for function objects.
Prefer to use lambda expressions to pass callbacks to standard algorithms instead of functions or function objects:
auto numbers =
std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 };
auto positives = std::count_if(
std::begin(numbers), std::end(numbers),
[](int const n) {return n > 0; });
auto ispositive = [](int const n) {return n > 0; };
auto positives = std::count_if(
std::begin(numbers), std::end(numbers), ispositive);
auto positives = std::count_if(
std::begin(numbers), std::end(numbers),
[](auto const n) {return n > 0; });
The non-generic lambda expression shown above takes a constant integer and returns true if it is greater than 0, or false otherwise. The compiler defines an unnamed function object with the call operator having the signature of the lambda expression.
struct __lambda_name__
{
bool operator()(int const n) const { return n > 0; }
};
The way the unnamed function object is defined by the compiler depends on the way we define the lambda expression, that can capture variables, use the mutable specifier or exception specifications or may have a trailing return type. The __lambda_name__ function object shown earlier is actually a simplification of what the compiler generates because it also defines a default copy and move constructor, a default destructor, and a deleted assignment operator.
In the next example, we want to count the number of elements in a range that are greater or equal to 5 and less or equal than 10. The lambda expression, in this case, will look like this:
auto numbers = std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 };
auto start{ 5 };
auto end{ 10 };
auto inrange = std::count_if(
std::begin(numbers), std::end(numbers),
[start,end](int const n)
{return start <= n && n <= end;});
This lambda captures two variables, start and end, by copy (that is, value). The result unnamed function object created by the compiler looks very much like the one we defined above. With the default and deleted special members mentioned earlier, the class looks like this:
class __lambda_name_2__
{
int start_;
int end_;
public:
explicit __lambda_name_2__(int const start, int const end) :
start_(start), end_(end)
{}
__lambda_name_2__(const __lambda_name_2__&) = default;
__lambda_name_2__(__lambda_name_2__&&) = default;
__lambda_name_2__& operator=(const __lambda_name_2__&)
= delete;
~__lambda_name_2__() = default;
bool operator() (int const n) const
{
return start_ <= n && n <= end_;
}
};
The lambda expression can capture variables by copy (or value) or by reference, and different combinations of the two are possible. However, it is not possible to capture a variable multiple times and it is only possible to have & or = at the beginning of the capture list.
The following table shows various combinations for the lambda captures semantics.
Lambda | Description |
[](){} | Does not capture anything |
[&](){} | Captures everything by reference |
[=](){} | Captures everything by copy |
[&x](){} | Capture only x by reference |
[x](){} | Capture only x by copy |
[&x...](){} | Capture pack extension x by reference |
[x...](){} | Capture pack extension x by copy |
[&, x](){} | Captures everything by reference except for x that is captured by copy |
[=, &x](){} | Captures everything by copy except for x that is captured by reference |
[&, this](){} | Captures everything by reference except for pointer this that is captured by copy (this is always captured by copy) |
[x, x](){} | Error, x is captured twice |
[&, &x](){} | Error, everything is captured by reference, cannot specify again to capture x by reference |
[=, =x](){} | Error, everything is captured by copy, cannot specify again to capture x by copy |
[&this](){} | Error, pointer this is always captured by copy |
[&, =](){} | Error, cannot capture everything both by copy and by reference |
The general form of a lambda expression, as of C++17, looks like this:
[capture-list](params) mutable constexpr exception attr -> ret
{ body }
All parts shown in this syntax are actually optional except for the capture list, that can, however, be empty, and the body, that can also be empty. The parameter list can actually be omitted if no parameters are needed. The return type does not need to be specified as the compiler can infer it from the type of the returned expression. The mutable specifier (that tells the compiler the lambda can actually modify variables captured by copy), the constexpr specifier (that tells the compiler to generate a constexpr call operator) and the exception specifiers and attributes are all optional.
There are cases when lambda expressions only differ in the type of their arguments. In this case, the lambdas can be written in a generic way, just like templates, but using the auto specifier for the type parameters (no template syntax is involved).
Functions are a fundamental concept in programming; regardless the topic we discussed we end up writing functions. This article contains recipes related to functions. This article, however, covers modern language features related to functions and callable objects.
Further resources on this subject: