Daily bit(e) of C++ | Learn Modern C++ 2/N
Daily bit(e) of C++ #90 , A Modern-only C++ course (including C++23), part 2 of N
Welcome to the second lesson of the Learn Modern C++ sub-series. Today we will go over the main building blocks of C++ programs.
If you missed the previous lesson, check it out here:
Hello World
As a warmup, let’s have a look at a “hello world”:
While C++20 introduced support for modules, the support in compilers is not quite there yet, so for this course, we will be sticking with headers, which is the first line.
We will discuss how exactly C++ code gets compiled and linked together later in the course, but for now, remember that you will need to add an include directive to access a particular part of the standard library. The iostream header provides standard input and output streams.
Then we have a main function. This is our entry point which will get automatically called on program start.
Inside the main function, we use the stream insertion operator with std::cout. std::cout represents the standard output, and as a side effect of the expression evaluation, the text “Hello World!” will be printed on the standard output.
That’s a lot of technical terms right here, so let’s slow down and cover these topics individually.
Variables, constants and lifetimes
C++ might be a bit of a shock if you have experience with more dynamic languages (such as Python or Javascript). Mutable-value semantics is one of the main culprits.
Let’s have a look at a straightforward example with two integer variables:
Since the two variables are separate entities (in C++ terms: two unique lvalues), initializing one with the other copies the value, with the two entities remaining separate. Each variable will have a unique memory address (unless eliminated by the optimizer).
Constants
The word constant is slightly overloaded in C++. In this context, it is better to talk about immutable values. C++ has two types of immutable values that differ in how they can be initialized:
Const variables can be initialized at runtime, and constexpr variables can only be initialized using a constant expression (an expression that can be evaluated at compile-time). This has a slightly strange consequence where not all const variables can participate in constant expressions (notably, we couldn’t initialize y
with x
in the above example).
You won’t need const variables for now, so when you need to represent a constant, stick with constexpr.
The situation gets more complex when we mix in reference semantics, but that is a topic for a later lesson.
Lifetimes
In C++, the lifetime of objects is tightly bound to their location in the code, notably the scope.
Variables are created when the control flow reaches the declaration of each variable and are destroyed when the surrounding scope ends (in the inverse order of construction).
For now, we will be working with simple types that do not have side effects on construction (or destruction). However, you can already take advantage of scopes to keep variables local.
Expressions
Expressions are combinations of operators and operands. C++ contains all the typical operators (arithmetic, logical, assignment, comparison). In addition, there are additional C++-specific operators, which we will cover later in the course.
I will not list the complete operator precedence; however, there shouldn’t be any surprises (complete operator precedence). Most operators are evaluated left-to-right; for now, the only operator you will be using that evaluates right-to-left is the assignment operator (and its compound versions).
Statements
Every expression can be turned into a statement. And in the previous section, the example code demonstrated statements (i.e. x = y
is an expression, x = y;
is a statement).
Statements are the imperative core of C++. Each function contains a series of statements executed in sequence (with the caveat that the optimizer can make unobservable changes).
The stream insertion decomposition might seem confusing since I’m not showing you one crucial aspect. The result of a stream insertion expression is the first operand. This means that as we evaluate the expression from left to right, we end up with the following:
std::cout << "Final value {" << v << "}\n"
// (std::cout << "Final value {") -> std::cout
std::cout << v << "}\n"
// (std::cout << v) -> std::cout
std::cout << "}\n"
Of course, the part we care about is that evaluating stream insertion with std::cout as the first argument has the side effect of printing text to standard output.
Functions
Very quickly, you will run into a situation where the amount of code in your main function will become cumbersome. We can create additional functions to delegate parts of our code for that purpose.
Each function has a return type, a name and a list of zero or more arguments with their types.
Functions declarations serve the simple purpose of announcing that a particular function exists. You will not need this now; however, you would put declarations into a header file.
Check the homework to see headers and function declarations in action. Each homework consists of a library (a header and an implementation file) and a test that uses the header.
Function definition then provides the actual implementation. Since C++ supports overloading, we can have multiple versions of a function under the same name. Still, each version must have a unique set of the number of arguments or their types (note: arguments only, the result type is not considered).
Value semantics with functions
Since we are sticking with value semantics for now, it is worth noting that the function arguments are local variables, and with value semantics, we end up with a copy of the value the function was called with.
Conditions
Flow control in C++ comes in the form of “if” statements.
Because “if” is a statement, we can chain multiple conditions.
If statements also allow for local variables to be declared before the condition itself, which is useful when we want to process the result of a function call.
C++ also offers two additional conditionals.
The switch statement is helpful when working with enumerations (we will cover it there).
The ternary operator can be used to form conditional expressions. However, we will avoid it in this course (and I recommend avoiding it altogether) as it tends to be very hard to read and, due to modern C++ features, is no longer necessary.
Loops
In this course, we will be mainly using the range-based for-loop. The loop will iterate over all elements of the provided range, making them available through the named variable. Note that we are still doing value semantics; therefore, the content of each element will be copied into the variable.
C-style loops have a more complex syntax. The init statement can declare and initialize new variables, followed by a condition tested before every iteration (the loop ending when the expression evaluates to false) and another expression evaluated after each iteration.
While it is typical to use the condition for a boundary check and the expression for incrementing an index variable, that is not required.
The range-based for-loop might seem a bit cumbersome now; however, to give you a sneak peek, its true power unlocks once we start using the standard library:
C++ also supports while loops in two variants, one that evaluates the condition before each loop and one that evaluates the condition after each loop.
Finally, all loops support the continue and break statements. The continue statement will advance the loop to the next iteration, and the break statement will exit the loop.
Commented example
Finally, let’s have a look at a more complex commented example:
Homework
The template repository with homework for this lesson is here: https://github.com/HappyCerberus/daily-bite-course-02.
As with all homework, you will need VSCode and Docker installed on your machine and follow the instructions from the first lesson.
The goal is to make all tests pass as described in the readme file.