11 min read

In this article by Marius Bancila, author of the book Modern C++ Programming Cookbook covers the following recipes:

  • Defaulted and deleted functions
  • Using lambdas with standard algorithms

(For more resources related to this topic, see here.)

Defaulted and deleted functions

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.

Getting started

For this recipe, you need to know what special member functions are, and what copyable and moveable means.

How to do it…

Use the following syntax to specify how functions should be handled:

  • To default a function use =default instead of the function body. Only special class member functions that have defaults can be defaulted.
    struct foo 
            { 
              foo() = default; 
            };
  • To delete a function use =delete instead of the function body. Any function, including non-member functions, can be deleted.
    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:

  • To implement a class that is not copyable, and implicitly not movable, declare the copy operations as deleted.
    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; 
            };
  • To implement a class that is not copyable, but it is movable, declare the copy operations as deleted and explicitly implement the move operations (and provide any additional constructors that are needed).
     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; 
                } 
            };
  • To ensure a function is called only with objects of a specific type, and perhaps prevent type promotion, provide deleted overloads for the function (the example below with free functions can also be applied to any class member functions).
    template <typename T> 
            void run(T val) = delete; 
      
            void run(long val) {} // can only be called with long integers

How it works…

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:

  • If a user defined constructor exists, the default constructor is not generated by default.
  • If a user defined virtual destructor exists, the default constructor is not generated by default.
  • If a user-defined move constructor or move assignment operator exist, then the copy constructor and copy assignment operator are not generated by default.
  • If a user defined copy constructor, move constructor, copy assignment operator, move assignment operator or destructor exist, then the move constructor and move assignment operator are not generated by default.
  • If a user defined copy constructor or destructor exists, then the copy assignment operator is generated by default.
  • If a user-defined copy assignment operator or destructor exists, then the copy constructor is generated by default.
Note that the last two are deprecated rules and may no longer be supported by your compiler.

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    
The rule of thumb (also known as The Rule of Five) for class special member functions is: if you explicitly define any of copy constructor, move constructor, copy assignment, move assignment or destructor then you must either explicitly define or default all of them.

Using lambdas with standard algorithms

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.

Getting ready

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.

How to do it…

Prefer to use lambda expressions to pass callbacks to standard algorithms instead of functions or function objects:

  • Define anonymous lambda expressions in the place of the call if you only need to use the lambda in a single place.
    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; });
  • Define a named lambda, that is, assigned to a variable (usually with the auto specifier for the type), if you need to call the lambda in multiple places.
     auto ispositive = [](int const n) {return n > 0; }; 
            auto positives = std::count_if( 
              std::begin(numbers), std::end(numbers), ispositive);
  • Use generic lambda expressions if you need lambdas that only differ in their argument types (available since C++14).
    auto positives = std::count_if( 
              std::begin(numbers), std::end(numbers),  
              [](auto const n) {return n > 0; });

How it works…

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.

It must be well understood that the lambda expression is actually a class. In order to call it, the compiler needs to instantiate an object of the class. The object instantiated from a lambda expression is called a lambda closure.

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.

A lambda can only capture variables from an enclosing function scope. It cannot capture variables with static storage duration (that means variables declared in namespace scope or with the static or external specifier).

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.

The simplest possible lambda expression is []{}, though it is often written as [](){}.

There’s more…

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).

Summary

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.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here