Sign In Start Free Trial
Account

Add to playlist

Create a Playlist

Modal Close icon
You need to login to use this feature.
  • Book Overview & Buying Expert C++
  • Table Of Contents Toc
Expert C++

Expert C++

By : Vardan Grigoryan, Shunguang Wu
3.1 (9)
close
close
Expert C++

Expert C++

3.1 (9)
By: Vardan Grigoryan, Shunguang Wu

Overview of this book

C++ has evolved over the years and the latest release – C++20 – is now available. Since C++11, C++ has been constantly enhancing the language feature set. With the new version, you’ll explore an array of features such as concepts, modules, ranges, and coroutines. This book will be your guide to learning the intricacies of the language, techniques, C++ tools, and the new features introduced in C++20, while also helping you apply these when building modern and resilient software. You’ll start by exploring the latest features of C++, and then move on to advanced techniques such as multithreading, concurrency, debugging, monitoring, and high-performance programming. The book will delve into object-oriented programming principles and the C++ Standard Template Library, and even show you how to create custom templates. After this, you’ll learn about different approaches such as test-driven development (TDD), behavior-driven development (BDD), and domain-driven design (DDD), before taking a look at the coding best practices and design patterns essential for building professional-grade applications. Toward the end of the book, you will gain useful insights into the recent C++ advancements in AI and machine learning. By the end of this C++ programming book, you’ll have gained expertise in real-world application development, including the process of designing complex software.
Table of Contents (22 chapters)
close
close
1
Section 1: Under the Hood of C++ Programming
7
Section 2: Designing Robust and Efficient Applications
17
Section 3: C++ in the AI World

Understanding preprocessing

A preprocessor is intended to process source files to make them ready for compilation. A preprocessor works with preprocessor directives, such as #define, #include, and so on. Directives don't represent program statements, but they are commands for the preprocessor, telling it what to do with the text of the source file. The compiler cannot recognize those directives, so whenever you use preprocessor directives in your code, the preprocessor resolves them accordingly before the actual compilation of the code begins. For example, the following code will be changed before the compiler starts to compile it:

#define NUMBER 41 
int main() {
int a = NUMBER + 1;
return 0;
}

Everything that is defined using the #define directive is called a macro. After preprocessing, the compiler gets the transformed source in this form:

int main() { 
int a = 41 + 1;
return 0;
}

As already mentioned, the preprocessor is just processing the text and does not care about language rules or its syntax. Using preprocessor directives, especially macro definitions, as in the previous example, #define NUMBER 41 is error-prone, unless you realize that the preprocessor simply replaces any occurrence of NUMBER with 41 without interpreting 41 as an integer. For the preprocessor, the following lines are both valid:

int b = NUMBER + 1; 
struct T {}; // user-defined type
T t = NUMBER; // preprocessed successfully, but compile error

This produces the following code:

int b = 41 + 1
struct T {};
T t = 41; // error line

When the compiler starts compilation, it finds the assignment t = 41 erroneous because there is no viable conversion from 'int' to 'T'.

It is even dangerous to use macros that are correct syntactically but have logical errors:

#define DOUBLE_IT(arg) (arg * arg) 

The preprocessor will replace any occurrence of DOUBLE_IT(arg) with (arg * arg), therefore the following code will output 16:

int st = DOUBLE_IT(4);
std::cout << st;

The compiler will receive this code as follows:

int st = (4 * 4);
std::cout << st;

Problems arise when we use complex expressions as a macro argument:

int bad_result = DOUBLE_IT(4 + 1); 
std::cout << bad_result;

Intuitively, this code will produce 25, but the truth is that the preprocessor doesn't do anything but text processing, and in this case, it replaces the macro like this:

int bad_result = (4 + 1 * 4 + 1);
std::cout << bad_result;

This outputs 9, and 9 is obviously not 25.

To fix the macro definition, surround the macro argument with additional parentheses:

#define DOUBLE_IT(arg) ((arg) * (arg)) 

Now the expression will take this form:

int bad_result = ((4 + 1) * (4 + 1)); 

It is strongly suggested to use const declarations instead of macro definitions wherever applicable.

As a rule of thumb, avoid using macro definitions. Macros are error-prone and C++ provides a set of constructs that make the use of macros obsolete.

The same preceding example would be type-checked and processed at compile time if we used a constexpr function:

constexpr int double_it(int arg) { return arg * arg; } 
int bad_result = double_it(4 + 1);

Use the constexpr specifier to make it possible to evaluate the return value of the function (or the value of a variable) at compile time. The example with the NUMBER definition would be better rewritten using a const variable:

const int NUMBER = 41; 

Header files

The most common use of the preprocessor is the #include directive, intended to include header files in the source code. Header files contain definitions for functions, classes, and so on:

// file: main.cpp 
#include <iostream>
#include "rect.h"
int main() {
Rect r(3.1, 4.05)
std::cout << r.get_area() << std::endl;
}

Let's suppose the header file rect.h is defined as follows:

// file: rect.h
struct Rect
{
private:
double side1_;
double side2_;
public:
Rect(double s1, double s2);
const double get_area() const;
};

The implementation is contained in rect.cpp:

// file: rect.cpp
#include "rect.h"

Rect::Rect(double s1, double s2)
: side1_(s1), side2_(s2)
{}

const double Rect::get_area() const {
return side1_ * side2_;
}

After the preprocessor examines main.cpp and rect.cpp, it replaces the #include directives with corresponding contents of iostream and rect.h for main.cpp and rect.h for rect.cpp. C++17 introduces the __has_include preprocessor constant expression. __has_include evaluates to 1 if the file with the specified name is found and 0 if not:

#if __has_include("custom_io_stream.h")
#include "custom_io_stream.h"
#else
#include <iostream>
#endif

When declaring header files, it's strongly advised to use so-called include-guards (#ifndef, #define, #endif) to avoid double declaration errors. We are going to introduce the technique shortly. Those are, again, preprocessor directives that allow us to avoid the following scenario: type Square is defined in square.h, which includes rect.h in order to derive Square from Rect:

// file: square.h
#include "rect.h"
struct Square : Rect {
Square(double s);
};

Including both square.h and rect.h in main.cpp leads to including rect.h twice:

// file: main.cpp
#include <iostream>
#include "rect.h"
#include "square.h"
/*
preprocessor replaces the following with the contents of square.h
*/
// code omitted for brevity

After preprocessing, the compiler will receive main.cpp in the following form:

// contents of the iostream file omitted for brevity 
struct Rect {
// code omitted for brevity
};
struct Rect {
// code omitted for brevity
};
struct Square : Rect {
// code omitted for brevity
};
int main() {
// code omitted for brevity
}

The compiler will then produce an error because it encounters two declarations of type Rect. A header file should be guarded against multiple inclusions by using include-guards in the following way:

#ifndef RECT_H 
#define RECT_H
struct Rect { ... }; // code omitted for brevity
#endif // RECT_H

When the preprocessor meets the header for the first time, RECT_H is not defined and everything between #ifndef and #endif will be processed accordingly, including the RECT_H definition. The second time the preprocessor includes the same header file in the same source file, it will omit the contents because RECT_H has already been defined.

These include-guards are part of directives that control the compilation of parts of the source file. All of the conditional compilation directives are #if, #ifdef, #ifndef, #else, #elif, and #endif.

Conditional compilation is useful in many cases; one of them is logging function calls in so-called debug mode. Before releasing the program, it is advised to debug your program and test against logical flaws. You might want to see what happens in the code after invoking a certain function, for example:

void foo() {
log("foo() called");
// do some useful job
}
void start() {
log("start() called");
foo();
// do some useful job
}

Each function calls the log() function, which is implemented as follows:

void log(const std::string& msg) {
#if DEBUG
std::cout << msg << std::endl;
#endif
}

The log() function will print the msg if DEBUG is defined. If you compile the project enabling DEBUG (using compiler flags, such as -D in g++), then the log() function will print the string passed to it; otherwise, it will do nothing.

Using modules in C++20

Modules fix header files with annoying include-guard issues. We can now get rid of preprocessor macros. Modules incorporate two keywords, import and export. To use a module, we import it. To declare a module with its exported properties, we use export. Before listing the benefits of using modules, let's look at a simple usage example. The following code declares a module:

export module test;

export int twice(int a) { return a * a; }

The first line declares the module named test. Next, we declared the twice() function and set it to export. This means that we can have functions and other entities that are not exported, thus, they will be private outside of the module. By exporting an entity, we set it public to module users. To use module, we import it as done in the following code:

import test;

int main()
{
twice(21);
}

Modules are a long-awaited feature of C++ that provides better performance in terms of compilation and maintenance. The following features make modules better in the competition with regular header files:

  • A module is imported only once, similar to precompiled headers supported by custom language implementations. This reduces the compile time drastically. Non-exported entities have no effect on the translation unit that imports the module.
  • Modules allow expressing the logical structure of code by allowing you to select which units should be exported and which should not. Modules can be bundled together into bigger modules.
  • Getting rid of workarounds such as include-guards, described earlier. We can import modules in any order. There are no more concerns for macro redefinitions.

Modules can be used together with header files. We can both import and include headers in the same file, as demonstrated in the following example:

import <iostream>;
#include <vector>

int main()
{
std::vector<int> vec{1, 2, 3};
for (int elem : vec) std::cout << elem;
}

When creating modules, you are free to export entities in the interface file of the module and move the implementations to other files. The logic is the same as in managing .h and .cpp files.

CONTINUE READING
83
Tech Concepts
36
Programming languages
73
Tech Tools
Icon Unlimited access to the largest independent learning library in tech of over 8,000 expert-authored tech books and videos.
Icon Innovative learning tools, including AI book assistants, code context explainers, and text-to-speech.
Icon 50+ new titles added per month and exclusive early access to books as they are being written.
Expert C++
notes
bookmark Notes and Bookmarks search Search in title playlist Add to playlist download Download options font-size Font size

Change the font size

margin-width Margin width

Change margin width

day-mode Day/Sepia/Night Modes

Change background colour

Close icon Search
Country selected

Close icon Your notes and bookmarks

Confirmation

Modal Close icon
claim successful

Buy this book with your credits?

Modal Close icon
Are you sure you want to buy this book with one of your credits?
Close
YES, BUY

Submit Your Feedback

Modal Close icon
Modal Close icon
Modal Close icon