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

Chapter 9 Design Patterns and Idioms

Idioms are typically used during detailed design and implementation, if programming problems must be solved at a low level of abstraction. A well-known idiom in the C and C++ domain is the so-called Include Guard, sometimes also called Macro Guard or Header Guard, which is used to avoid double inclusion of the same header file:

#ifndef FILENAME_H_ #define FILENAME_H_

// ...content of header file...

#endif

One disadvantage of this idiom is that a consistent naming scheme for filenames, and thus also for Include Guard macro names, must be ensured. Hence, most modern C and C++ compilers support a non-standard #pragma once directive. This directive, inserted at the top of a header file, will ensure that the header file is included only once.

By the way, we have already gotten to know a few idioms. In Chapter 4, we discussed the Resource Acquisition Is Initialization (RAII) idiom, and in Chapter 6, I presented the Type Erasure idiom that I also used earlier in this chapter to implement the State pattern.

Some Useful C++ Idioms

It is not a joke, but you can actually find an exhaustive collection of nearly 100(!) C++ idioms on the Internet (WikiBooks: More C++ Idioms; URL: https://en.wikibooks. org/wiki/More_C++_Idioms). The problem is that not all of these idioms are conducive to a modern and clean C++ program. They are sometimes very complex and barely comprehensible (e.g., Algebraic Hierarchy), even for fairly skilled C++ developers. Furthermore, some idioms have become largely obsolete by the publishing of C++11 and subsequent standards. Therefore, I present here a small selection, which I consider interesting and still useful.

The Power of Immutability

Sometimes it’s very helpful to have classes for objects that cannot change their states once they have been created, a.k.a. immutable classes (what is really meant by this are in fact immutable objects, because properly speaking a class can only be altered by a developer). For instance, immutable objects can be used as key values

436

Chapter 9 Design Patterns and Idioms

in a hashed data structure, since the key value should never change after creation. Another known example of an immutable object is the String class in several other languages like C# or Java.

The benefits of immutable classes and objects are the following:

•\

Immutable objects are thread-safe by default, so you will not have

 

any synchronization issues if several threads or processes access

 

those objects in a non-deterministic way. Thus, immutability makes

 

it easier to create a parallelizable software design as there are no

 

conflicts among objects.

•\

Immutability makes it easier to write, use, and reason about the code,

 

because a class invariant, that is, a set of constraints that must always

 

be true, is established once at object creation and is ensured to be

 

unchanged during the object’s lifetime.

To create an immutable class in C++, the following measures must be taken:

•\

The member variables of the class must all be made immutable,

 

that is, they must all be made const (see the section about const

 

correctness in Chapter 4). This means that they can only be

 

initialized once in a constructor, using the constructor’s member

 

initializer list.

•\

Manipulating methods do not change the object on which they are

 

called, but return a new instance of the class with an altered state.

 

The original object is not changed. To emphasize this, there should

 

be no setter, because a member function whose name starts with

 

set...() is misleading. There is nothing to set on an immutable

 

object.

•\

The class should be marked as final. This is not a hard rule, but if

 

a new class can be inherited from an allegedly immutable class, it

 

might be possible to circumvent its immutability (remember the

 

history constraint described in Chapter 4).

437

Chapter 9 Design Patterns and Idioms

Listing 9-43 shows an example of an immutable class in C++.

Listing 9-43.  Employee Is Designed as an Immutable Class

#include "Identifier.h" #include "Money.h"

#include <string>

#include <string_view>

class Employee final { public:

Employee(std::string_view forename, std::string_view surname,

const Identifier& staffNumber, const Money& salary) noexcept : forename { forename },

surname { surname }, staffNumber { staffNumber }, salary { salary } { }

Identifier getStaffNumber() const noexcept { return staffNumber;

}

Money getSalary() const noexcept { return salary;

}

Employee changeSalary(const Money& newSalary) const noexcept { return Employee(forename, surname, staffNumber, newSalary);

}

 

private:

 

const std::string

forename;

const std::string

surname;

const Identifier

staffNumber;

const Money

salary;

};

 

438

Chapter 9 Design Patterns and Idioms

Substitution Failure Is Not an Error (SFINAE)

In fact, substitution failure is not an error (SFINAE) is not a real idiom but a feature of the C++ compiler. It has already been a part of the C++98 standard, but with C++11 several new features have been added. However, it is still referred to as an idiom, also because it is used in a very idiomatic style, especially in template libraries, such as the C++ Standard Library, or Boost.

The defining text passage in the standard can be found in Section 14.8.2 about template argument deduction. There we can read in §8 the following statement:

“If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed if written using the substituted arguments. Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure.”

—Standard for Programming Language C++ [ISO11]

Error messages in case of a faulty instantiation of C++ templates, for example, with wrong template arguments, can be very verbose and cryptic. SFINAE is a programming technique that ensures that a failed substitution of template arguments does not create an annoying compilation error. Simply put, it means that if the substitution of a template argument fails, the compiler continues with the search for a suitable template instead of aborting with an error.

Listing 9-44 shows a very simple example with two overloaded function templates.

Listing 9-44.  SFINAE by Example of Two Overloaded Function Templates

#include <iostream>

template <typename T>

void print(typename T::type) {

std::cout << "Calling print(typename T::type)" << std::endl;

}

template <typename T> void print(T) {

std::cout << "Calling print(T)" << std::endl;

}

439

Chapter 9 Design Patterns and Idioms

struct AStruct { using type = int;

};

int main() { print<AStruct>(42); print<int>(42); print(42);

return 0;

}

The output of this small example on stdout will be:

Calling print(typename T::type)

Calling print(T)

Calling print(T)

As can be seen, the compiler uses the first version of print() for the first function call, and the second version for the two subsequent calls. This code also works in C++98. Well, but SFINAE prior C++11 had several drawbacks. The previous very simple example is a bit deceptive regarding the real effort to use this technique in real projects.

The application of SFINAE this way in template libraries has led to very verbose and tricky code that is difficult to understand. Furthermore, it is badly standardized and sometimes compiler specific.

With the advent of C++11, the so-called Type Traits library was introduced, which we got to know in Chapter 7. The meta function std::enable_if() (defined in the <type_ traits> header), which is available since C++11, played a central role in SFINAE. With this function we got a conditionally “remove functions capability” from overload resolution based on type traits. C++14 added a helper template for std::enable_if

that has a shorter syntax: std::enable_if_t. With the help of this template, and the miscellaneous template-based type checks from the <type_traits> header, we can, for example, pick an overloaded version of a function depending on the argument’s type at compile-time. See Listing 9-45.

440

Chapter 9 Design Patterns and Idioms

Listing 9-45.  SFINAE By Using the Function Template std::enable_if<>

#include <iostream> #include <type_traits>

template <typename T>

void print(T var, std::enable_if_t<std::is_enum_v<T>, T>* = nullptr) { std::cout << "Calling overloaded print() for enumerations." << std::endl;

}

template <typename T>

void print(T var, std::enable_if_t<std::is_integral_v<T>, T> = 0) { std::cout << "Calling overloaded print() for integral types." << std::endl;

}

template <typename T>

void print(T var, std::enable_if_t<std::is_floating_point_v<T>, T> = 0.0) { std::cout << "Calling overloaded print() for floating point types." << std::endl;

}

template <typename T>

void print(const T& var, std::enable_if_t<std::is_class_v<T>, T>* = nullptr) {

std::cout << "Calling overloaded print() for classes." << std::endl;

}

The overloaded function templates can be used by simply calling them with arguments of different types, as shown in Listing 9-46.

Listing 9-46.  Thanks to SFINAE, There Is a Matching print() Function for Arguments of Different Types

enum Enumeration1 { Literal1, Literal2

};

441

Chapter 9 Design Patterns and Idioms

enum class Enumeration2 : int { Literal1,

Literal2

};

class Clazz { };

int main() {

Enumeration1 enumVar1 { }; print(enumVar1);

Enumeration2 enumVar2 { }; print(enumVar2);

print(42);

Clazz instance { }; print(instance);

print(42.0f);

print(42.0);

return 0;

}

After compiling and executing, we see the following result on the standard output:

Calling overloaded print() for enumerations.

Calling overloaded print() for enumerations.

Calling overloaded print() for integral types.

Calling overloaded print() for classes.

Calling overloaded print() for floating point types.

Calling overloaded print() for floating point types.

442