Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt.pdf
Скачиваний:
29
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 5 Advanced Concepts of Modern C++

•\ std::execution::unseq (since C++20). An execution policy type that defines that a parallel algorithm’s execution may be vectorized, i.e., the algorithm takes advantage of SIMD and can perform the same operation on multiple data elements simultaneously.

Of course, it makes absolutely no sense to sort a small vector with a few elements in parallel. The overhead for thread management would be much higher than the gain on performance. Thus, an execution policy should also be selectable dynamically during runtime, for example, by taking the size of the vector into consideration. Unfortunately, as it was the case when the C++17 Standard was adopted, so-called dynamic execution policies are also not included in C++20.

A full discussion of all available algorithms is way beyond the scope of this book. But after this short introduction to the <algorithm> header and the advanced possibilities of parallelization with C++20, let’s look at a few examples of what can be done with algorithms.

Sorting and Output of a Container

The following example uses two templates from the <algorithm> header: std::sort and std::for_each. Internally, std::sort is using the quicksort algorithm. By default, the comparisons inside std::sort are performed with the operator< function of the elements. This means that if you want to sort a sequence of instances of one of your own classes, you have to ensure that operator< is properly implemented on that type. See Listing 5-25.

Listing 5-25.  Sorting a Vector of Strings and Printing Them on stdout

#include <algorithm> #include <iostream>

#include <string>

#include <string_view>

#include <vector>

void printCommaSeparated(std::string_view text) { std::cout << text << ", ";

}

188

Chapter 5 Advanced Concepts of Modern C++

int main() {

std::vector<std::string> names = { "Peter", "Harry", "Julia", "Marc", "Antonio", "Glenn" };

std::sort(begin(names), end(names)); std::for_each(begin(names), end(names), printCommaSeparated); return 0;

}

But couldn’t this be even easier? Yes it could!

More Convenience with Ranges

Maybe you have sometimes also asked yourself why there is no more comfortable API for the algorithms than always calling them with two iterators of a container, usually the start and the end iterator. After all, applying an algorithm to all elements in a container or sequence is probably the most common use case.

Maybe you’ve heard about the so-called Range Library for C++14/17/20, written by Eric Niebler, a member of the ISO C++ Standardization Committee. Eric’s library code became the basis of a formal proposal to add range support to the C++ Standard Library. It was merged into the C++20 working drafts in November 2018 and finally became part of the C++20 standard.

C++20 Ranges is a header-only library that simplifies the dealing with containers of the C++ Standard Library or containers from other libraries (e.g., Boost). With the help of this library, you can get rid of the sometimes tricky juggling with iterators in various situations. For instance, instead of writing:

std::sort(std::begin(container), std::end(container));

you can simply write:

std::ranges::sort(container);

With the help of ranges, the example in Listing 5-25 can be implemented more simply and becomes much more readable, as shown in Listing 5-26.

189

Chapter 5 Advanced Concepts of Modern C++

Listing 5-26.  Sorting and Printing a Vector of Strings with the Help of Ranges

#include <algorithm> #include <iostream>

#include <ranges> #include <string>

#include <string_view>

#include <vector>

void printCommaSeparated(std::string_view text) { std::cout << text << ", ";

}

int main() {

std::vector<std::string> names = { "Peter", "Harry", "Julia", "Marc", "Antonio", "Glenn" };

std::ranges::sort(names); std::ranges::for_each(names, printCommaSeparated); return 0;

}

For many algorithms from the <algorithm> header that require iterators as parameters, there is a corresponding alternative with this simplified interface in the std::ranges namespace. But C++20 Ranges offers even more: Views!

Non-Owning Ranges with Views

Containers from the C++ Standard Library are owners of their elements. For instance, if you delete a std::vector, all the elements stored in it are also deleted.

In contrast, views are a category of ranges that do not own any element. Views can be applied to other ranges, or to subareas of these ranges, and provide a kind of “transformed view” onto the elements in the underlying range. These “transformed views” are generated by algorithms or operations.

It is important to know that views are lazy-evaluated, i.e. whatever transformation they apply to the underlying range, they do so at the moment users request an element, not when the view is created! In other words, applying the std::reverse algorithm on a container manipulates the ordering of its elements immediately, whereas applying std::views::reverse on the same container doesn’t change it in this moment:

190

Chapter 5 Advanced Concepts of Modern C++

#include <iostream>

#include <ranges> #include <vector>

std::vector<int> integers = { 2, 5, 8, 22, 45, 67, 99 };

auto view = std::views::reverse(integers); // does not change 'integers'

The proof that the view does not manipulate the underlying range can be provided by outputting the first element of the view and vector to stdout:

std::cout << *view.begin() << ", " << *integers.begin() << '\n';

The output is as follows:

99, 2

It must be emphasized again that the computation that the first element of the view view corresponds to the last element of the vector named integers is done on demand. This also reveals something that needs to be considered when using views: if the same element is requested again, the same transformation has to be performed again! This can lead to performance losses, especially with complex transformations.

That’s all for now; you will learn about a few more features of ranges in Chapter 7 on functional programming.

Comparing Two Sequences

The example in Listing 5-27 compares two sequences of strings using std::equal.

Listing 5-27.  Comparing Two Sequences of Strings

#include <algorithm> #include <iostream>

#include <string> #include <vector>

int main() {

const std::vector<std::string> names1 { "Peter", "Harry", "Julia", "Marc", "Antonio", "Glenn" };

191

Chapter 5 Advanced Concepts of Modern C++

const std::vector<std::string> names2 { "Peter", "Harry", "Julia", "John", "Antonio", "Glenn" };

const bool isEqual = std::equal(begin(names1), end(names1), begin(names2), end(names2));

if (isEqual) {

std::cout << "The contents of both sequences are equal.\n";

}else {

std::cout << "The contents of both sequences differ.\n";

}

return 0;

}

By default, std::equal compares elements using operator==. But you can define “equalness” as you want. The standard comparison can be replaced with a custom comparison operation, as shown in Listing 5-28.

Listing 5-28.  Comparing Two Sequences of Strings Using a Custom Predicate Function

#include <algorithm> #include <iostream>

#include <string> #include <vector>

bool compareFirstThreeCharactersOnly(const std::string& string1, const std::string& string2) {

return (string1.compare(0, 3, string2, 0, 3) == 0);

}

int main() {

const std::vector<std::string> names1 { "Peter", "Harry", "Julia", "Marc", "Antonio", "Glenn" };

const std::vector<std::string> names2 { "Peter", "Harold", "Julia", "Maria", "Antonio","Glenn" };

192

Chapter 5 Advanced Concepts of Modern C++

const bool isEqual = std::equal(begin(names1), end(names1), begin(names2),

end(names2), compareFirstThreeCharactersOnly);

if (isEqual) {

std::cout << "The first three characters of all strings in both sequences are equal.\n";

} else {

std::cout << "The first three characters of all strings in both sequences differ.\n";

}

return 0;

}

If no reusability is required for the comparison function compareFirstThreeCharactersOnly(), the line where the comparison takes place can also be implemented using a lambda expression, like this:

// Compare just the first three characters of every string to ascertain equalness:

const bool isEqual =

std::equal(begin(names1), end(names1), begin(names2), end(names2), [](const auto& string1, const auto& string2) {

return (string1.compare(0, 3, string2, 0, 3) == 0); });

We discuss lambda expressions in more detail in Chapter 7. This alternative may appear more compact, but it does not necessarily contribute to the readability of the code. The explicit function compareFirstThreeCharactersOnly() has a semantic name that expresses very clearly what is compared (not the how; see the section “Use Intention-Revealing Names” in Chapter 4). What exactly is compared cannot necessarily be seen at first sight from the version with the lambda expression. Always keep in mind that the readability of our code should be one of our first goals. Also keep in mind that source code comments are basically a code smell and not suitable to explain hard-to-­ read code (remember the section about comments in Chapter 4).

193