Key Takeaways:
-
C++ is still very important and will always be.
-
There are many resources to help us learn modern C++, including Godbolt’s Compiler Explorer, ISOCpp, and CppReference.
-
C++ can be simpler than before. Besides enhancements related to convenience, potential performance improvements are also a driving force behind C++11 and subsequent standards.
-
We practice by filling a vector and outputting its contents. We further exercise by using algorithms, ranges, and lambdas on the vector to find elements with specific attributes.
C++ is an old yet evolving language. You can use it to do almost anything, and you can find it almost everywhere. In fact, Bjarne Stroustrup, the inventor of C++, described it as the invisible foundation of everything. Sometimes, it can even delve into the libraries of another language, as C++ can be used in performance-critical paths. It can run in small embedded systems or power video games. Your browser might be using it. C++ is virtually everywhere!
So far, C++ has been around for a long time, but it has also changed significantly, especially since 2011. At that time, a new standard called C++11 was introduced, marking the beginning of an era of frequent updates. If you haven’t used C++ since C++11, then you have a lot to catch up on. Where should you start?
The language needs to be compiled and is targeted at specific architectures, such as PCs, mainframes, embedded devices, custom hardware, or anything else you can think of. If you need the code to run on different types of machines, it needs to be recompiled. This has both drawbacks and advantages. Different configurations bring more maintenance work, but compiling to specific architectures allows you to go “down to the metal,” gaining speed advantages.
No matter what platform you target, you need a compiler. You also need an editor or an Integrated Development Environment (IDE) to write C++ code. ISOCpp provides a list of resources that include C++ compilers. The Gnu Compiler Collection (GCC), Clang, and Visual Studio all have free versions. You can even use Matt Godbolt’s Compiler Explorer to try out code based on various compilers in your browser. Compilers may support different versions of C++, so you must specify the version you need in the compiler flags, such as g++’s -std=c++23 or Visual Studio’s /std:c++latest. The ISOCpp website has a FAQ section that outlines some recent changes, including C++11 and C++14, as well as an overall overview. Additionally, there are several books about the recent versions of C++.
If you’ve fallen behind, the abundance of resources might overwhelm you. However, we can understand some basics through a small example. Stopping to try things out yourself is often the best way to learn. So, let’s start with something simple!
A very useful (and simple) starting point is the unassuming vector, which is located in the std namespace’s vector header file. CppReference provides an overview that tells us vector is a sequence container that encapsulates dynamic-sized arrays. Therefore, vector contains a contiguous sequence of elements, and we can adjust the size of the vector as needed. The vector itself is a class template, so it requires a type, such as std::vector
std::vector<int> numbers; numbers.push_back(1); numbers.emplace_back(1);</int>
If we have something more complex than int, we might gain performance benefits in the emplace version, as the emplace version can construct entries in place, avoiding copies.
C++11 introduced r-value references and move semantics to avoid unnecessary copies. Potential performance improvements are one of the driving forces behind C++11, and subsequent versions build on this. To explain what r-value references are, we can consider the push_back method from the previous example. It has two overloaded forms, one accepting a constant reference, i.e., const T& value, and the other accepting an r-value reference, i.e., T&& value. The second version moves the element into the vector, avoiding copies of temporary objects. Similarly, the signature of emplace_back takes parameters via r-value references, Args&&…, allowing for moving parameters without copies. Move semantics is a large topic, and we have only scratched the surface. If you want to know more details, Thomas Becker wrote a great article in 2013 that covers its intricacies.
We create a vector and place a few entries in it, then use std::cout from the iostream header file to display its contents. We use the stream insertion operator << to show these elements. We write a for loop based on the vector‘s size and access each element using the [] operator:
#include <iostream> #include <vector> void warm_up() { std::vector<int> numbers; numbers.push_back(1); numbers.emplace_back(1); for(int i=0; i<numbers.size(); '="" ';="" ++i)="" <<="" code="" int="" main()="" numbers[i]="" std::cout="" warm_up();="" {="" }="" }<=""></numbers.size();></int></vector></iostream>
This code will display two 1s. You can find this code in the Compiler Explorer.
Let’s do something more interesting and learn modern C++. We can build a number triangle and notice a pattern among them. The values of the number triangle are 1, 3, 6, 10… which are obtained by summing 1, 1+2, 1+2+3, 1+2+3+4, and so on. If we arrange these snooker balls, we can form a triangle, hence the name:
If we add another row, we will add six more snooker balls. Adding another row will add seven, and so on.
To get the numbers 1, 2, 3, etc., we can construct a vector filled with 1s and then sum these numbers. We can directly create a vector of, say, 18 ones, without needing another loop. We specify how many elements we want and then indicate its value:
std::vector numbers(18, 1);
Note that we don’t need to declare
for (auto i : numbers) { std::cout << i << ' '; } std::cout << '
';
CTAD and range-based for loops are some of the convenient features introduced since C++11.
With the vector filled with “1”, we can include the numeric header file and use partial sums to fill a new vector, such as 1, 1+1, 1+1+1…, thus yielding 1, 2, 3…. We need to declare the type of the new vector because we are starting from an empty vector. If there are no values to use, the compiler will not be able to deduce its type. partial_sum requires the starting and ending numbers, and finally, we need to use back_inserter, so that the target vector grows as needed:
#include <algorithm>... std::vector numbers(18, 1); std::vector<int> sums; std::partial_sum(numbers.begin(), numbers.end(), std::back_inserter(sums));</int></algorithm>
This way, we get the numbers from 1 to 18, including the boundary values. We have completed part of the work for the number triangle, but C++ can now make our code more concise. C++11 introduced the iota function, also located in the numeric header file, which can fill a container with increasing values:
std::vector<int> sums(18); std::iota(sums.begin(), sums.end(), 1);</int>
In fact, C++23 introduced a range version that will find the corresponding begin and end for us:
std::ranges::iota(sums, 1);
C++23 is not yet widely supported, so you may need to wait until your compiler provides the range version. Many algorithms in the numeric and algorithm headers have two versions, one requiring a pair of input iterators (i.e., first and last), and the other is a range version that only accepts the container. Range overloads are gradually being added to standard C++. The functionality provided by ranges far exceeds our scenario of avoiding declaring two iterators. We can filter and transform outputs, concatenate these things together, and use views to avoid copying data. Ranges support lazy evaluation, so the contents of views are computed only when needed. Ivan Čukić‘s book Functional Programming in C++ provides more details in this regard (and includes more content).
The last thing we need to do is form the number triangle. We look at the partial sums of the vector:
std::partial_sum(sums.begin(), sums.end(), sums.begin());
We have obtained the desired number triangle, which is 1, 3, 6, 10, 15… 171.
We notice that some algorithms have range versions, so we can try one. The first two triangular numbers are 1 and 3 (odd), followed by two even numbers 6 and 10. Is this pattern sustainable? If we transform the vector using a dot “.” to mark odd numbers and an asterisk “*” to mark even numbers, we can see the final result. We can declare a new vector to store the transformation results. For each number, we only need one character, so we need a char type vector:
std::vector<char> odd_or_even;</char>
We can write a short function that takes an int and returns the corresponding character:
char flag_odd_or_even(int i) { return i % 2 ? '.' : '*'; }
If the value of i % 2 is not zero, it is an odd number, so we return ‘.’, otherwise we return ‘*’. We can use this function in the transform function from the algorithm header file. The original version requires a pair of input iterators (first and last), an output iterator, and a unary function (unary function) that accepts an input, just like our flag_odd_or_even function. C++20 introduced a range version that can accept an input source instead of a pair of iterators, plus an output iterator and a unary function. This means we can transform the previously generated sums like this:
std::vector<char> odd_or_even; std::ranges::transform(sums, std::back_inserter(odd_or_even), flag_odd_or_even);</char>
The output will look like this:
. . * * . . * * . . * * . . * * . .
It seems that we are indeed getting two odd numbers followed by two even numbers continuously. The mathematics site Stack Exchange explains why this phenomenon occurs.
We use another new C++ feature to make the final improvement to our code. If we want to see the actual transformation code, we need to go to another place to see what this unary function does.
C++11 introduced the feature of anonymous functions or lambda expressions. They look similar to named functions, placing parameters in parentheses and the function body in curly braces, but they have no names, do not require a return type, and have a capture group indicated by []:
[](int i) { return i % 2 ? '.' : '*'; }
Comparing with a named function, we can see the similarity:
char flag_odd_or_even(int i) { return i % 2 ? '.' : '*'; }
We can declare variables in the capture group, giving us a closure. These contents go beyond the scope of this article, but they are very powerful and common in functional programming.
If we assign a lambda to a variable:
auto lambda = [](int i) { return i % 2 ? '.' : '*'; };
Then we can call it just like a named function:
lambda(7);
This feature allows us to rewrite the transform call using the lambda:
std::ranges::transform(sums, std::back_inserter(odd_or_even), [](int i) { return i % 2 ? '.' : '*'; });
This way, we can see the transformation function in one place without having to look elsewhere.
Bringing everything together, we have the following code:
#include <algorithm> #include <iostream> #include <numeric> #include <vector> int main() { std::vector<int> sums(18); std::iota(sums.begin(), sums.end(), 1); std::partial_sum(sums.begin(), sums.end(), sums.begin()); std::vector<char> odd_or_even; std::ranges::transform(sums, std::back_inserter(odd_or_even), [](int i) { return i % 2 ? '.' : '*'; }); for (auto c : odd_or_even) { std::cout << c << ' '; } std::cout << '
'; }</char></int></vector></numeric></iostream></algorithm>
We used ranges, lambdas, and range-based for loops, explored move semantics, and practiced using vector. This is a good starting point for those returning to C++ for the first time!
You can try the above code in the Compiler Explorer.
Frances Buontempo, has years of experience with C++, as well as experience using Python and various other languages. She has given talks on C++ and is an editor for ACCU’s Overload magazine. She has a background in mathematics and has written a book on genetic algorithms and machine learning for PragProg, and is currently writing a book called C++ Bookcamp for Manning to help those who have fallen behind modern C++ catch up.
Original Link:
Relearning C++ After C++11 (https://www.infoq.com/articles/relearning-cpp-11/)
Disclaimer: This article is translated by InfoQ and reproduction without permission is prohibited.
Dialogue with Wang Wenjing of Yongyou, exploring the “key” to enterprise digital intelligence
Is Electron’s doom coming? Another application abandons it! WhatsApp enforces native applications: faster, less memory usage
Exclusive dialogue with Kingdee’s Li Fan: How will enterprise-level PaaS platforms lead technological innovation in enterprises?
Red Hat deals a devastating blow to RHEL downstream! Stopping public enterprise version source code, aiming to occupy open source share for profit?