Reader small image

You're reading from  Modern C++ Programming Cookbook - Third Edition

Product typeBook
Published inFeb 2024
PublisherPackt
ISBN-139781835080542
Edition3rd Edition
Right arrow
Author (1)
Marius Bancila
Marius Bancila
author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila

Right arrow

Robustness and Performance

C++ is often the first choice when it comes to selecting an object-oriented programming language with performance and flexibility as key goals. Modern C++ provides language and library features, such as rvalue references, move semantics, and smart pointers.

When combined with good practices for exception handling, constant correctness, type-safe conversions, resource allocation, and releasing, C++ enables developers to write better, more robust, and performant code. This chapter’s recipes address all of these essential topics.

This chapter includes the following recipes:

  • Using exceptions for error handling
  • Using noexcept for functions that do not throw exceptions
  • Ensuring constant correctness for a program
  • Creating compile-time constant expressions
  • Creating immediate functions
  • Optimizing code in constant-evaluated contexts
  • Using virtual function calls in constant expressions
  • Performing correct...

Using exceptions for error handling

Exceptions are responses to exceptional circumstances that can appear when a program is running. They enable the transfer of the control flow to another part of the program. Exceptions are a mechanism for simpler and more robust error handling, as opposed to returning error codes, which could greatly complicate and clutter the code. In this recipe, we will look at some key aspects related to throwing and handling exceptions.

Getting ready

This recipe requires you to have basic knowledge of the mechanisms of throwing exceptions (using the throw statement) and catching exceptions (using try...catch blocks). This recipe is focused on good practices around exceptions and not on the details of the exception mechanism in the C++ language.

How to do it...

Use the following practices to deal with exceptions:

  • Throw exceptions by value:
    void throwing_func()
    {
      throw std::runtime_error("timed out");
    }
    void another_throwing_func...

Using noexcept for functions that do not throw exceptions

Exception specification is a language feature that can enable performance improvements, but on the other hand, when done incorrectly, it can abnormally terminate the program. The exception specification from C++03, which allowed you to indicate what types of exceptions a function could throw, was deprecated in C++11 and removed in C++17. It was replaced with the C++11 noexcept specifier. Moreover, the use of the throw() specifier to indicate that a function throws, without indicating what exception types have also been deprecated in C++17 and completely removed in C++20. The noexcept specifier only allows you to indicate that a function does not throw exceptions (as opposed to the old throw specifier, which could indicate the list of types a function could throw). This recipe provides information about the modern exception specifications in C++, as well as guidelines on when to use them.

How to do it...

Use the following...

Ensuring constant correctness for a program

Although there is no formal definition, constant correctness means objects that are not supposed to be modified (are immutable) remain unmodified. As a developer, you can enforce this by using the const keyword for declaring parameters, variables, and member functions. In this recipe, we will explore the benefits of constant correctness and how to achieve it.

How to do it...

To ensure constant correctness for a program, you should always declare the following as constants:

  • Parameters to functions that are not supposed to be modified within the function:
    struct session {};
    session connect(std::string const & uri,
                    int const timeout = 2000)
    {
      /* do something */
      return session { /* ... */ };
    }
    
  • Class data members that do not change:
    class user_settings
    {
    public:
      int const min_update_interval = 15;
      /* other members */
    };
    
  • Class member functions that do not...

Creating compile-time constant expressions

The possibility to evaluate expressions at compile time improves runtime execution because there is less code to run and the compiler can perform additional optimizations. Compile-time constants can be not only literals (such as a number or string), but also the result of a function’s execution. If all the input values of a function (regardless of whether they are arguments, locals, or global variables) are known at compile time, the compiler can execute the function and have the result available at compile time. This is what generalized the constant expressions that were introduced in C++11, which were relaxed in C++14 and even further in C++20. The keyword constexpr (short for constant expression) can be used to declare compile-time constant objects and functions. We have seen this in several examples in the previous chapters. Now, it’s time to learn how it actually works.

Getting ready

The way generalized constant...

Creating immediate functions

constexpr functions enable the evaluation of functions at compile time, provided that all their inputs, if any, are also available at compile time. However, this is not a guarantee, and constexpr functions may also execute at runtime, as we have seen in the previous recipe, Creating compile-time constant expressions. In C++20, a new category of functions has been introduced: immediate functions. These are functions that are guaranteed to always be evaluated at compile time; otherwise, they produce errors. Immediate functions are useful as replacements for macros and may be important in the possible future development of the language with reflection and meta-classes.

How to do it…

Use the consteval keyword when you want to:

  • Define non-member functions or function templates that must be evaluated at compile time:
    consteval unsigned int factorial(unsigned int const n)
    {
      return n > 1 ? n * factorial(n-1) : 1;
    }
    
    ...

Optimizing code in constant-evaluated contexts

In the previous two recipes, we learned about constexpr functions, which allow functions to be evaluated at compile time if all their inputs are available at compile time, and immediate functions (in C++20), which are guaranteed to always be evaluated at compile time (or otherwise, produce an error). An important aspect of constexpr functions is constant-evaluated contexts; these are code paths where all expressions and functions are evaluated at compile time. A constant-evaluated context is useful for optimizing code more effectively. On the other hand, the invocation of immediate functions from constexpr functions is only possible in C++23. In this recipe, we will learn about utilizing constant-evaluated contexts.

How to do it…

To determine whether a function context is constant-evaluated in order to provide compile-time implementations use the following:

  • In C++20, the std::is_constant_evaluated() library function...

Using virtual function calls in constant expressions

As a multi-paradigm programming language, C++ includes support for object-oriented programming. Polymorphism, one of the core principles of object-oriented programming, has two forms in C++: compile-time polymorphism, with function and operator overloading, and runtime-polymorphism, with virtual functions. Virtual functions allow a derived class to override the implementation (of a function) in the base class. In C++20, however, virtual functions are allowed in constant expressions, meaning they can be invoked at compile time. In this recipe, you will learn how that works.

Getting ready

In this recipe, we will use the following structure to represent the dimension of a document and, respectively, an envelope, in the ensuing examples:

struct dimension
{
   double width;
   double height;
};

How to do it…

You can move runtime polymorphism to the compile time by doing the following:

  • Declare the...

Performing correct type casts

It is often the case that data has to be converted from one type into another type. Some conversions are necessary at compile time (such as double to int); others are necessary at runtime (such as upcasting and downcasting pointers to the classes in a hierarchy). The language supports compatibility with the C casting style in either the (type)expression or type(expression) form. However, this type of casting breaks the type safety of C++.

Therefore, the language also provides several conversions: static_cast, dynamic_cast, const_cast, and reinterpret_cast. They are used to better indicate intent and write safer code. In this recipe, we’ll look at how these casts can be used.

How to do it...

Use the following casts to perform type conversions:

  • Use static_cast to perform type casting of non-polymorphic types, including the casting of integers to enumerations, from floating-point to integral values, or from a pointer type to...

Implementing move semantics

Move semantics is a key feature that drives the performance improvements of modern C++. They enable moving, rather than copying, resources, or, in general, objects that are expensive to copy. However, it requires that classes implement a move constructor and move assignment operator. These are provided by the compiler in some circumstances, but in practice, it is often the case that you have to explicitly write them. In this recipe, we will see how to implement the move constructor and the move assignment operator.

Getting ready

You are expected to have basic knowledge of rvalue references and the special class functions (constructors, assignment operators, and destructors). We will demonstrate how to implement a move constructor and assignment operator using the following Buffer class:

class Buffer
{
  unsigned char* ptr;
  size_t length;
public:
  Buffer(): ptr(nullptr), length(0)
  {}
  explicit Buffer(size_t const size):
    ptr(new unsigned...

Using unique_ptr to uniquely own a memory resource

Manual handling of heap memory allocation and releasing it (with new and delete) is one of the most controversial features of C++. All allocations must be properly paired with a corresponding delete operation in the correct scope. If the memory allocation is done in a function and needs to be released before the function returns, for instance, then this has to happen on all the return paths, including the abnormal situation where a function returns because of an exception. C++11 features, such as rvalues and move semantics, have enabled the development of better smart pointers (since some, such as auto_ptr, existed prior to C++11); these pointers can manage a memory resource and automatically release it when the smart pointer is destroyed. In this recipe, we will look at std::unique_ptr, a smart pointer that owns and manages another object or an array of objects allocated on the heap, and performs the disposal operation when the smart...

Using shared_ptr to share a memory resource

Managing dynamically allocated objects or arrays with std::unique_ptr is not possible when the object or array has to be shared. This is because a std::unique_ptr retains its sole ownership. The C++ standard provides another smart pointer, called std::shared_ptr; it is similar to std::unique_ptr in many ways, but the difference is that it can share the ownership of an object or array with other std::shared_ptr objects. In this recipe, we will see how std::shared_ptr works and how it differs from std::uniqueu_ptr. We will also look at std::weak_ptr, which is a non-resource-owning smart pointer that holds a reference to an object managed by a std::shared_ptr.

Getting ready

Make sure you read the previous recipe, Using unique_ptr to uniquely own a memory resource, to become familiar with how unique_ptr and make_unique() work. We will use the foo, foo_deleter, Base, and Derived classes defined in this recipe, and also make several references...

Consistent comparison with the operator <=>

The C++ language defines six relational operators that perform comparison: ==, !=, <, <=, >, and >=. Although != can be implemented in terms of ==, and <=, >=, and > in terms of <, you still have to implement both == and != if you want your user-defined type to support equality comparison, and <, <=, >, and >= if you want it to support ordering.

That means 6 functions if you want objects of your type—let’s call it T—to be comparable, 12 if you want them to be comparable with another type, U, 18 if you also want values of a U type to be comparable with your T type, and so on. The new C++20 standard reduces this number to either one or two, or multiple of these (depending on the comparison with other types) by introducing a new comparison operator, called the three-way comparison, which is designated with the symbol <=>, for which reason it is popularly known as the spaceship...

Comparing signed and unsigned integers safely

The C++ language features a variety of integral types: short, int, long, and long long, as well as their unsigned counterparts unsigned short, unsigned int, unsigned long, and unsigned long long. In C++11, fixed-width integer types were introduced, such as int32_t and uint32_t, and many similar others. Apart from these, there are also the types char, signed char, unsigned char, wchar_t, char8_t, char16_t, and char32_t, although these are not supposed to store numbers but characters. Moreover, the type bool used for storing the values true or false is also an integral type. The comparison of values of these types is a common operation but comparing signed and unsigned values is error-prone. Without some compiler-specific switches to flag these as warnings or errors, you can perform these operations and get unexpected results. For instance, the comparison -1 < 42u (comparing signed -1 with unsigned 42) would yield false. The C++20 standard...

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Modern C++ Programming Cookbook - Third Edition
Published in: Feb 2024Publisher: PacktISBN-13: 9781835080542
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila