ua

Tasks for independent work

Using Modules and Lambda Expressions

1 Training Tasks

1.1 Representation and Processing of Data about Students using the Tools of the Standard Template Library, Modules and Lambda Expressions

Complete task 1.1 of the fifth laboratory training (Classes for representing a student and a group) using the tools of the Standard Library of templates, modules and lambda expressions. Place classes for student and group presentation in separate modules. Use import from modules instead of connecting header files.

1.2 Using Lambda Expressions for Callbacks

Solve task 1.3 of the fourth laboratory training . Use lambda expressions instead of functional objects. Place the function that calculates the required result according to the individual task in a separate module. Use import from modules instead of connecting header files.

2 Instructions

2.1 Additional Features of C++ Language Introduced in the Latest Versions

The C++ language was first standardized in 1998. In 2003, a new version of the C++ standard was published that fixed the problems identified in C++98.

On August 12, 2011, the new official standard for C++ was approved. A new standardized version of the language includes many interesting features:

  • Automatic type inference
  • Initializer lists
  • Range-based for loop
  • Lambda functions and expressions
  • Alternative function syntax
  • Extension of constructor syntax
  • Explicitly overriding virtual functions (override modifier after the override function header)
  • Null pointer constant nullptr
  • Strongly typed enumerations
  • Local and anonymous types as template arguments
  • Unicode characters and strings
  • Raw string literals
  • Possibility of garbage collected implementations
  • Using compiler attributes

There are also additional syntactic nuances and differences in the internal organization of the language kernel.

Versions C++14 and C++17 expanded the scope and clarified the work of new syntactic structures, as well as expanded the capabilities of the Standard Library.

The C++20 standard was approved on September 4, 2020. It was officially published in December 2020. Among the new possibilities, the following should be noted:

  • Concepts
  • Modules
  • Three-way comparison
  • New standard attributes
  • Immediate functions with a consteval modifier

Also added special constructions related to multithreading. Also improved syntax constructs added in previous versions.

The next standard should be published in 2023.

Some of the listed innovations will be considered below.

2.2 Automatic Type Inference

As mentioned before, C++11 has added a mechanism of automatic type inference. This mechanism enables compiler to create local variables, which types depend on the initialization expression type. To create these variables the auto keyword is used. For example:

auto i = 10;      // integer variable
auto s = "Hello"; // pointer to character

These variables must be initialized.

In the above example, using auto is not very reasonable, but for complex type names, this approach is quite convenient:

map<string, int> mm;
map<string, int>::iterator iter1 = mm.begin(); // previous form
auto iter2 = mm.begin(); // new form

The mechanism of obtaining information about the type of variable for further use of this type is connected with automatic definition of types. This mechanism is implemented by the decltype keyword. For example, you can create a variable of the same type as the previous one:

long long int n = 5000000000;
decltype(n) m;

The auto keyword can be used to describe the result type of functions and function-elements, if they are non-virtual:

auto sum(int a, int b)
{
    return a + b;
}

class AutoTest
{
public:
    auto product(int a, int b)
    {
        return a * b;
    }
};

The ability to describe the type of function using auto has been added since C++14.

Use of automatic type inference is not always desirable because it does not improve code visibility and can lead to unexpected results.

2.3 Initializer Lists

Traditionally initializer lists used to initialize arrays and structures. For example, in all previous versions allowed such initialization:

struct SomeData
{
    double d;
    int i;
};

SomeData data  = { 0.1, 1 }; 
SomeData arr[] = { { 0.0, 0 },
                   { 0.1, 1 },
                   { 0.2, 2 } };

The extension of the capabilities of the initializer lists is associated with the template class std::initializer_list. Objects of the corresponding type are implicit constants. Most often this type is used to describe the function parameters, in particular, arguments of constructors. Such constructors are added to existing collection classes. For example:

vector<int> a({ 1, 2 });
vector<int> b = { 3, 4 };

The initializer list can be bypassed using a range-based loop. You can create a function with the appropriate parameter, for example:

int sum(std::initializer_list<int> inilist)
{
    int result = 0;
    for (const int &k : inilist)
    {
        result += k;
    }
    return result;
}

void main()
{
    std::cout << sum({ 1, 10, 12, 23 }); //46
}

2.4 Lambda Expressions as Function Objects

The C++11 language standard provides the syntax for lambda expressions that was covered in Laboratory training #4 of the first semester.

Lambdas are not only anonymous functions, but also functional objects. They can store some data copied from local context. This feature is called "capturing the context" and used for creation variables that values are stored between function calls. The following example shows the usage of the capture list item (the value of k is added to each array item):

int k = 10;
int a[] = { 1, 2, 3 };
for_each(a, a + 3, [k](int &x) { x += k; }); 
// now values of items are 11 12 13

You can also create reference to variable. This way you can reimplement an example concerned with storing items in a text file using the for_each() algorithm:

vector<int> a = { 1, 2, 3, 4 };
ofstream out("result.txt");
for_each(a.begin(), a.end(), [&out](const int& i) { out << i << endl; });

2.5 Using the constexpr Keyword

In all versions of C++, it is possible to create constants of two types:

  • constants that can be calculated by the compiler and then used to determine the size of the array, as a template argument, etc. (compile-time constants);
  • constants that can only be calculated at runtime (runtime constants).

For example, the constant m can be used to create an array, but n cannot:

const int m = 2 + 3;
const int n = 5.0;
int a[m];
int b[n]; // error

Also, you cannot use the result of a function to create compile-time constants:

int mValue()
{
    return 5;
}

int main()
{
    const int m = mValue();
    double arr[m]; // error
}

It is also not possible to use static elements of structures and classes:

struct IntValue
{
    static int n = 5;
};

. . .

const int n = IntValue::n;
int b[n]; // error

The presence of two versions of constants, the definitions of which are very similar, introduces unwanted confusion. Also, evaluating some complex expressions at compile time would improve the overall performance of the program. Therefore, starting with C++11, a new keyword constexpr has been added. It allows you to explicitly define expressions that the compiler can theoretically evaluate. Example:

constexpr int n = 5.5 * 2.0;
int b[n];

In subsequent versions, the possibilities of constexpr were expanded.

A constexpr modifier can be added to members of data types and functions. In the following example, the factorial() function can be applied at both run-time and compile-time to obtain the value of a constant. Loops can be located inside the function:

#include <iostream>

constexpr int factorial(int n)
{
    if (n < 0 || n > 12)
    {
        throw std::range_error("argument out of range");
    }
    int result = 1;
    for (int i = 1; i <= n; i++)
    {
        result *= i;
    }
    return result;
}

int main()
{
    int x = 4;
    std::cout << factorial(x) << std::endl;
    constexpr int m = factorial(5); // or const int m = factorial(5);
    double arr[m]; // array 120 items
    arr[119] = 3;
    std::cout << arr[119] << std::endl;
    return 0;
}

You can use recursion to implement the factorial() function:

constexpr int factorial(int n)
{
    if (n < 0 || n > 12)
    {
        throw std::range_error("argument out of range");
    }
    if (n < 2) {
        return 1;
    }
    return n * factorial(n - 1);
}

The result will be the same.

Since exception handling is not possible at compile time, using an invalid argument like

constexpr int m = factorial(14);

will result in a compiler error.

In addition to the constexpr modifier, the C++20 version also added the consteval modifier. Functions marked with this modifier can only be used at compile time. For example, only the compiler can call the corresponding function:

consteval double reciprocal(double x) 
{
    return 1 / x;
}

. . .

constexpr int n = reciprocal(0.25); // OK
double arr[n]; // array of four 4 itemsdouble x = 2;
double y = reciprocal(x); // error

An attempt to call the reciprocal(x) function resulted in a compile error.

2.6 Move Semantics. Using rvalue-References

Many innovations in C++11 are aimed at improving the efficiency of program execution. The move semantics of the move assume that in cases where the value of the source variable is no longer used during the assignment, the movement can be performed instead of copying. The special function std::move() is used for this purpose. In addition, a so-called rvalue reference is added to the syntax. In describing it, && is used instead of one &.

Using such references reduces the number of different memory cells required. For example, the variable c does not create a new memory cell: it uses a cell created for the constant:

int&& c = 10;

In the following snippet, the variable refers to a temporary object that contains the value calculated by the sin() function:

double x;
cin >> x;
double&& y = sin(x);

You can modify the previously implemented swap() function to exchange the values of two variables as follows:

void swap(int& a, int& b)
{
    int&& c = std::move(a);
    a = std::move(b);
    b = std::move(c);
}

Now no extra variable is created.

2.7 Extension of Constructor Syntax

The C++11 language standard expands the capabilities of constructors. It was considered the ability to call constructors from other constructors in the initialization list. In addition, the so-called "move constructor" has been added. This constructor is not created automatically. The syntax of the move constructor for the Demo class will be as follows:

Demo(Demo&& d);

To implement the move semantics, you should also redefine the move assignment operator:

Demo operator=(Demo&& d);

You can also add default and delete modifiers to constructor headers. This simplifies the choice between a constructor without parameters and a constructor with default parameters as a default, and, for example, allows you to prohibit copying objects:

Demo(int k = 0) { }
Demo() = default;
Demo(const Demo&) = delete;

The syntax for "deleted function" can also be applied to other member functions, for example, if we want to disable the use of an automatically defined operator:

class DeleteTest
{
public:
    DeleteTest operator=(const DeleteTest& dt) = delete;
};

int main()
{
    DeleteTest dt1, dt2;
    dt1 = dt2; // Syntax error
}

2.8 Use of Modules

Implementation of the paradigm of modular programming in C/C++ languages through the use of header files and implementation files has significant disadvantages:

  • increased compilation time;
  • intrusive inclusion of header files: if a header file links others, all inclusion are made in the resulting translation unit;
  • occurrence of unnecessary name conflicts;
  • possible occurrence of cyclical dependencies;
  • different compilation results depending on the inclusion order of the header files;
  • inclusion macros and using unwanted preprocessor directives, which can adversely affect the understanding, compilation, and execution of the program.

Modules are designed to overcome these drawbacks. A module a special new construction of the language that appeared in C++20.

The module consists of an interface unit and an implementation unit. A module's interface unit is a source code file that contains a header, declarations of types, constants, variables, and functions that the module exports, and, if necessary, types, constants, variables, and functions for internal use. For example, the interface unit of the module can be as follows:

export module my.tools;  // module header

export int one();
export int two() 
{ 
    return 2;
}

export const int three = 3;
export int count;

int zero()  // for internal use, not exported
{
    return 0;
}

Note: the internal name of the module is not related to the file name, the dot in the name is added for convenience.

An implementation unit is a source code file that specifies the belonging of a particular module and the implemented functions declared in the interface unit. Adding new declarations for export is prohibited.

module my.tools;  // indicate which module this file belongs to

int one()
{
    return zero() + two() - 1;
}

In a main()function (outside the module), you can import and use the types, constants, variables, and functions that the module exports:

import my.tools;

int main()
{
    int k = one();    // OK
    k += zero();      // error
    count = k + three;
    return count;
}

You can also create modules that consist of separate parts (partition files).

In order to create a module in the Visual Studio 2022 environment, you must first add the interface unit of the module (Project | Add New Item... | C++ Module Interface Unit). This is a file with the ixx extension. Next, if necessary, add the implementation file (Project | Add New Item... | C++ File (.cpp)).

In recent versions of C++, you can also use a module that provides most of the tools of the C++ Standard Library, in particular, for working with threads, containers, algorithms, etc. Instead of inclusion of header files to the source code, you can add

import std;

Note: in previous versions, the name of this module was std.core.

In order for code with this import to be compiled and executed, certain settings must be made in the Visual Studio 2022 environment. Use the Visual Studio Installer that was used when Visual Studio was installed to add the necessary tools. After starting the program, click the Install button on the Available tab. Next, select the Individual components panel and find the C++ Modules for v143 build tools component and click Modify .

In addition, you should configure the properties: Project | Properties, then in the Property Pages window open Configuration Properties | C/C++ | Language and set the following options::

  • C++ Language Standard: Preview - Features from the Latest C++ Working Draft (/std:c++latest)
  • Enable Experimental C++ Standard Library Modules: Yes (/experimental:module)

Now you can create a program that uses the std module:

import std;

int main()
{
    std::cout << "Hello Modules!\n";
    return 0;
}

The use of modules is expected to expand in future versions of C++.

3 Sample Programs

3.1 Creating a Class with a Constructor of Type initializer_list

Suppose we create our own wrapper class for a vector and want the vector to be initialized by a list. We can add the appropriate constructor to the class template:

#include <iostream>
#include <vector>

template <typename T> class Array1D
{
    friend std::ostream& operator<<(std::ostream& out, const Array1D<T> a)
    {
        for (const T &elem : a.vect)
        {
            out << elem << " ";
        }
        return out;
    }
private:
    std::vector<T> vect;
public:
    Array1D(std::initializer_list<T> list)
    {
        std::vector<T> newVect(list);
        vect = std::move(newVect);
    }
};

int main()
{
    Array1D<int> a = { 1, 2, 3 };
    std::cout << a;
    return 0;
}

In addition to list initialization, this example also illustrates the use a range-based loop, as well as the use of move semantics.

3.2 Using Modules, Tools of the Standard Template Library and Lambda Expressions to Represent Cities and Countries

In the laboratory training No. 5, an example of creating classes for representing a city and a country using the tools of the Standard Template Library was given. Now, classes City and Country can be placed in separate modules. In a project with support for the latest C++ capabilities and a module of the standard library (instructions for setting up the environment are given in 2.8), we create a new module city with an interface file City.ixx:

export module city;

import std;

using std::string;

export class Country;

// Class to represent the city
export class City
{
    // Overloaded operator for output to a stream
    friend std::ostream & operator<<(std::ostream & out, const City & city);
private:
    string name = "";          // city name
    Country* country = nullptr;// pointer to country of location
    string region = "";        // region name
    int population = 0;        // population
public:
    // Constructors:
    City() { }
    City(const char* name, const char* region, int population) :
        name(name), country(country), region(region), population(population) { }

    // Getters:
    string getName() const { return name; }
    Country* getCountry() const { return country; }
    string getRegion() const { return region; }
    int getPopulation() const { return population; }

    // Setters:
    void setName(string name) { this->name = name; }
    void setName(const char* name) { this->name = name; }
    void setRegion(string region) { this->region = region; }
    void setRegion(const char* region) { this->region = region; }
    void setCountry(Country* country) { this->country = country; }
    void setPopulation(int population) { this->population = population; }
};

We define the overloaded output operation in the implementation unit (City.cpp):

module city;

import country;

// Overloaded operator for output to a stream:
std::ostream& operator<<(std::ostream& out, const City& city)
{
    out << "City: " << city.name << "\tCountry: " << city.country->getName() 
        << "\t Region: " << city.region << "\t Population: " << city.population;
    return out;
}

We also create a module called country with an interface unit Country.ixx:

export module country;

import std;
import city;

using std::string;
using std::vector;

// Class to represent the country
export class Country
{
    // Overloaded operator for output to a stream
    friend std::ostream& operator<<(std::ostream& out, const Country& country);
private:
    string name;               // name of country
    vector<City> cities = { }; // array of pointers to cities
public:
    // Constructors:
    Country() { }
    Country(string name) : name(name) { }
    Country(const char* name) : name(name) { }
    string getName() const { return name; } // getter

    // Overloaded operator for getting array items
    City operator[](int index) const { return cities[index]; }

    // Setters:
    void setName(string name) { this->name = name; }
    void setName(const char* name) { this->name = name; }

    void addCity(City city); // Adding the city
    void sortByPopulation(); // Sort by population
};

In the implementation unit (file Country.cpp), we define an overloaded operation of output to a stream, adding a city, and sorting a vector of cities. In the output operator function, we use a range-based loop. The addition function uses move semantics. To sort cities, we use the sort() standard library algorithm. To determine the sorting criterion, we create a lambda expression:

module country;

using std::endl;

// Overloaded operator for output to a stream:
std::ostream& operator<<(std::ostream& out, const Country& country)
{
    out << country.name << endl;
    for (const City& city : country.cities)
    {
        out << city << endl;
    }
    out << endl;
    return out;
}

void Country::addCity(City city) // Adding the city
{
    city.setCountry(this); 
    cities.push_back(std::move(city));
}

void Country::sortByPopulation() // Sort by population
{
    std::sort(cities.begin(), cities.end(),
        [](City c1, City c2) { return c1.getPopulation() < c2.getPopulation(); });
}

In the file with the main() function, we create a country object, add cities and demonstrate the implemented capabilities of the classes:

import std;
import city;
import country;

int main()
{
    Country country = "Ukraine"; // create the Country object,
                                 // call the constructor with one parameter
    // Create and add cities:
    country.addCity(City("Kharkiv", "Kharkiv region", 1421125));
    country.addCity(City("Poltava", "Poltava region", 284942));
    country.addCity(City("Lozova", "Kharkiv region", 54618);
    country.addCity(City("Sumy", "Sumy region", 264753));

    std::cout << country << std::endl;   // output all data
    std::cout << country[0] << std::endl;// display information about the city by index
    std::cout << std::endl;
    country.sortByPopulation();          // sort
    std::cout << country << std::endl;   // output all data

    return 0;
}

3.3 Using Modules, Templates and Lambda Expressions in the Problem of Solving an Arbitrary Equation

Previously, the problem of solving an arbitrary equation by the dichotomy method was considered. In particular, in example 3.3 of laboratory training No. 4 there was a decision based on the use of templates. This example can be modified by adding C++20 modules and demonstrating how to define functions via lambda expressions. We create a new module. The interface unit of this module (file Dichotomy.ixx) will contain a module with the name dichotomy and code of the equation solving function that we export:

export module dichotomy;

export template <typename F> double root(F f, double a, double b, double eps = 0.001)
{
    double x;
    do
    {
        x = (a + b) / 2;
        if (f(a) * f(x) > 0)
        {
            a = x;
        }
        else
        {
            b = x;
        }
    } 
    while (b - a > eps);
    return x;
}

Since root() is the template function, it is implemented in the interface unit. An implementation file is not required.

In the file with function, we import the modules std and dichotomy , the first function that defines the left side of the equation, we define as a lambda expression. The left side of the second equation is a standard function sin(x). The program code will be as follows:

import std;
import dichotomy;

#include <math.h>

int main()
{
    std::cout << std::setprecision(14); // determine the number of characters to display
    // We define the left part of one of the equations with a lambda expression:
    std::cout << root([](double x) { return x * x - 2; }, 0, 2, 0.1E-14) << std::endl;
    // The left part of the second equation is the standard function sin(x):
    std::cout << root(sin, 1, 4, 0.1E-14) << std::endl;
    return 0;
}

The result of the program is the output of the values of the square root of 2 and the number π with sufficient precision:

1.4142135623731
3.1415926535898

Note: in order to prevent compilation errors, we use math.h instead of cmath header file

4 Exercises

  1. Create a program in which all calculations are performed at compile time.
  2. Develop a program in which a sequence is entered, and a new sequence is formed from the squares of the original values. Use the transform() algorithm and lambda expressions.

5 Quiz

  1. What new features does the C++11 standard provide?
  2. What new features does the C++20 standard provide?
  3. What is automatic type detection?
  4. What is automatic type inference?
  5. What is initializer list?
  6. What is lambda expression?
  7. What is the usage of lambda expressions?
  8. For what purpose the constexpr keyword is used?
  9. What is the move semantics?
  10. What are rvalue references used for?
  11. What is a move constructor?
  12. What are the advantages of modules compared to header files?
  13. What is a module interface unit?
  14. What is a module implementation unit?

 

up