Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Refactoring with C++
Refactoring with C++

Refactoring with C++: Explore modern ways of developing maintainable and efficient applications

eBook
$21.99 $31.99
Paperback
$27.98 $39.99
Subscription
Free Trial
Renews at $19.99p/m

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
OR
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Table of content icon View table of contents Preview book icon Preview Book

Refactoring with C++

Main Software Development Principles

In this chapter, we will explore the main software design principles that are used to create well-structured and maintainable code. One of the most important principles is SOLID, which stands for Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle. These principles are designed to help developers create code that is easy to understand, test, and modify. We will also discuss the importance of levels of abstraction, which is the practice of breaking down complex systems into smaller, more manageable parts. Additionally, we will explore the concepts of side effects and mutability and how they can affect the overall quality of software. By understanding and applying these principles, developers can create software that is more robust, reliable, and scalable.

SOLID

SOLID is a set of principles that were first introduced by Robert C. Martin in his book Agile Software Development, Principles, Patterns, and Practices, in 2000. Robert C. Martin, also known as Uncle Bob, is a software engineer, author, and speaker. He is considered one of the most influential figures in the software development industry, known for his work on the SOLID principles and his contributions to the field of object-oriented programming. Martin has been a software developer for more than 40 years and has worked on a wide variety of projects, from small systems to large enterprise systems. He is also a well-known speaker and has given presentations on software development at many conferences and events around the world. He is an advocate of agile methodologies, and he has been influential in the development of the Agile Manifesto. The SOLID principles were developed as a way to help developers create more maintainable and scalable code by promoting good design practices. The principles were based on Martin’s experience as a software developer and his observation that many software projects suffer from poor design, which makes them difficult to understand, change, and maintain over time.

The SOLID principles are intended to be a guide for object-oriented software design, and they are based on the idea that software should be easy to understand, change, and extend over time. The principles are meant to be applied in conjunction with other software development practices, such as test-driven development and continuous integration. By following SOLID principles, developers can create code that is more robust, less prone to bugs, and easier to maintain over time.

The Single Responsibility Principle

The Single Responsibility Principle (SRP) is one of the five SOLID principles of object-oriented software design. It states that a class should have only one reason to change, meaning that a class should have only one responsibility. This principle is intended to promote code that is easy to understand, change, and test.

The idea behind the SRP is that a class should have a single, well-defined purpose. This makes it easier to understand the class’s behavior and makes it less likely that changes to the class will have unintended consequences. When a class has only one responsibility, it is also less likely to have bugs, and it is easier to write automated tests for it.

Applying the SRP can be a useful way to improve the design of a software system by making it more modular and easier to understand. By following this principle, a developer can create classes that are small, focused, and easy to reason about. This makes it easier to maintain and improve the software over time.

Let us look at a messaging system that supports multiple message types sent over the network. The system has a Message class that receives sender and receiver IDs and raw data to be sent. Additionally, it supports saving messages to the disk and sending itself via the send method:

class Message {
public:
  Message(SenderId sender_id, ReceiverId receiver_id,
          const RawData& data)
    : sender_id_{sender_id},
      receiver_id_{receiver_id}, raw_data_{data} {}
  SenderId sender_id() const { return sender_id_; }
  ReceiverId receiver_id() const { return receiver_id_; }
  void save(const std::string& file_path) const {
    // serializes a message to raw bytes and saves
    // to file system
  }
  std::string serialize() const {
    // serializes to JSON
    return {"JSON"};
  }
  void send() const {
    auto sender = Communication::get_instance();
    sender.send(sender_id_, receiver_id_, serialize());
  }
private:
  SenderId sender_id_;
  ReceiverId receiver_id_;
  RawData raw_data_;
};

The Message class is responsible for multiple concerns, such as saving messages from/to the filesystem, serializing data, sending messages, and holding the sender and receiver IDs and raw data. It would be better to separate these responsibilities into different classes or modules.

The Message class is only responsible for storing the data and serializing it to JSON format:

class Message {
public:
  Message(SenderId sender_id, ReceiverId receiver_id,
          const RawData& data)
    : sender_id_{sender_id},
      receiver_id_{receiver_id}, raw_data_{data} {}
  SenderId sender_id() const { return sender_id_; }
  ReceiverId receiver_id() const { return receiver_id_; }
  std::string serialize() const {
    // serializes to JSON
    return {"JSON"};
  }
private:
  SenderId sender_id_;
  ReceiverId receiver_id_;
  RawData raw_data_;
};

The save method can be extracted to a separate MessageSaver class, having a single responsibility:

class MessageSaver {
public:
  MessageSaver(const std::string& target_directory);
  void save(const Message& message) const;
};

And the send method is implemented in a dedicated MessageSender class. All three classes have a single and clear responsibility, and any further changes in any of them would not affect the others. This approach allows isolating the changes in the code base. It becomes crucial in a complex system requiring long compilation.

In summary, the SRP states that a class should have only one reason to change, meaning that a class should have only one responsibility. This principle is intended to promote code that is easy to understand, change, and test, and it helps in creating a more modular, maintainable, and scalable code base. By following this principle, developers can create classes that are small, focused, and easy to reason about.

Other applications of the SRP

The SRP can be applied not only to classes but also to larger components, such as applications. At the architecture level, the SRP is often implemented as microservices architecture. The idea of microservices is to build a software system as a collection of small, independent services that communicate with each other over a network rather than building it as a monolithic application. Each microservice is responsible for a specific business capability and can be developed, deployed, and scaled independently from the other services. This allows for greater flexibility, scalability, and ease of maintenance, as changes to one service do not affect the entire system. Microservices also enable a more agile development process, as teams can work on different services in parallel, and also allows for a more fine-grained approach to security, monitoring, and testing, as each service can be handled individually.

The Open-Closed Principle

The Open-Closed principle states that a module or class should be open for extension but closed for modification. In other words, it should be possible to add new functionality to a module or class without modifying its existing code. This principle helps to promote software maintainability and flexibility. An example of this principle in C++ is the use of inheritance and polymorphism. A base class can be written with the ability to be extended by derived classes, allowing for new functionality to be added without modifying the base class. Another example is using interfaces or abstract classes to define a contract for a set of related classes, allowing new classes to be added that conform to the contract without modifying existing code.

The Open-closed Principle can be used to improve our message-sending components. The current version supports only one message type. If we want to add more data, we need to change the Message class: add fields, hold a message type as an additional variable, and not to mention serialization based on this variable. In order to avoid changes in existing code, let us rewrite the Message class to be purely virtual, providing the serialize method:

class Message {
public:
  Message(SenderId sender_id, ReceiverId receiver_id)
    : sender_id_{sender_id}, receiver_id_{receiver_id} {}
  SenderId sender_id() const { return sender_id_; }
  ReceiverId receiver_id() const { return receiver_id_; }
  virtual std::string serialize() const = 0;
private:
  SenderId sender_id_;
  ReceiverId receiver_id_;
};

Now, let us assume that we need to add another two message types: a “start” message supporting start delay (often done for debugging purposes) and a “stop” message supporting stop delay (can be used for scheduling); they can be implemented as follows:

class StartMessage : public Message {
public:
  StartMessage(SenderId sender_id, ReceiverId receiver_id,
               std::chrono::milliseconds start_delay)
    : Message{sender_id, receiver_id},
      start_delay_{start_delay} {}
  std::string serialize() const override {
    return {"naive serialization to JSON"};
  }
private:
  const std::chrono::milliseconds start_delay_;
};
class StopMessage : public Message {
public:
  StopMessage(SenderId sender_id, ReceiverId receiver_id,
              std::chrono::milliseconds stop_delay)
    : Message{sender_id, receiver_id},
      stop_delay_{stop_delay} {}
  std::string serialize() const override {
    return {"naive serialization to JSON"};
  }
private:
  const std::chrono::milliseconds stop_delay_;
};

Note that none of the implementations requires changes in other classes, and each of them provides its own version of the serialize method. The MessageSender and MessageSaver classes do not need additional adjustments to support the new class hierarchy of messages. However, we are going to change them too. The main reason is to make them extendable without requiring changes. For example, a message can be saved not only to the filesystem but also to remote storage. In this case, MessageSaver becomes purely virtual:

class MessageSaver {
public:
  virtual void save(const Message& message) const = 0;
};

The implementation responsible for saving to the filesystem is a class derived from MessageSaver:

class FilesystemMessageSaver : public MessageSaver {
public:
  FilesystemMessageSaver(const std::string&
    target_directory);
  void save(const Message& message) const override;
};

And the remote storage saver is another class in the hierarchy:

class RemoteMessageSaver : public MessageSaver {
public:
    RemoteMessageSaver(const std::string&
      remote_storage_address);
    void save(const Message& message) const override;
};

The Liskov Substitution Principle

The Liskov Substitution Principle (LSP) is a fundamental principle in object-oriented programming that states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. This principle is also known as the Liskov principle, named after Barbara Liskov, who first formulated it. The LSP is based on the idea of inheritance and polymorphism, where a subclass can inherit the properties and methods of its parent class and can be used interchangeably with it.

In order to follow the LSP, subclasses must be “behaviorally compatible” with their parent class. This means that they should have the same method signatures and follow the same contracts, such as input and output types and ranges. Additionally, the behavior of a method in a subclass should not violate any of the contracts established in the parent class.

Let’s consider a new Message type, InternalMessage, which does not support the serialize method. One might be tempted to implement it in the following way:

class InternalMessage : public Message {
public:
    InternalMessage(SenderId sender_id, ReceiverId
      receiver_id)
        : Message{sender_id, receiver_id} {}
    std::string serialize() const override {
        throw std::runtime_error{"InternalMessage can't be
          serialized!"};
    }
};

In the preceding code, InternalMessage is a subtype of Message but cannot be serialized, throwing an exception instead. This design is problematic for a few reasons:

  • It breaks the Liskov Substitution Principle: As per the LSP, if InternalMessage is a subtype of Message, then we should be able to use InternalMessage wherever Message is expected without affecting the correctness of the program. By throwing an exception in the serialize method, we are breaking this principle.
  • The caller must handle exceptions: The caller of serialize must handle exceptions, which might not have been necessary when dealing with other Message types. This introduces additional complexity and the potential for errors in the caller code.
  • Program crashes: If the exception is not properly handled, it could lead to the program crashing, which is certainly not a desirable outcome.

We could return an empty string instead of throwing an exception, but this still violates the LSP, as the serialize method is expected to return a serialized message, not an empty string. It also introduces ambiguity, as it’s not clear whether an empty string is the result of a successful serialization of a message with no data or an unsuccessful serialization of InternalMessage.

A better approach is to separate the concerns of a Message and a SerializableMessage, where only SerializableMessages have a serialize method:

class Message {
public:
    virtual ~Message() = default;
    // other common message behaviors
};
class SerializableMessage : public Message {
public:
    virtual std::string serialize() const = 0;
};
class StartMessage : public SerializableMessage {
    // ...
};
class StopMessage : public SerializableMessage {
    // ...
};
class InternalMessage : public Message {
    // InternalMessage doesn't have serialize method now.
};

In this corrected design, the base Message class does not include a serialize method, and a new SerializableMessage class has been introduced that includes this method. This way, only messages that can be serialized will inherit from SerializableMessage, and we adhere to the LSP.

Adhering to the LSP allows for more flexible and maintainable code, as it enables the use of polymorphism and allows for substituting objects of a class with objects of its subclasses without affecting the overall behavior of the program. This way, the program can take advantage of the new functionality provided by the subclass while maintaining the same behavior as the superclass.

The Interface Segregation Principle

The Interface Segregation Principle (ISP) is a principle in object-oriented programming that states that a class should only implement the interfaces it uses. In other words, it suggests that interfaces should be fine-grained and client-specific rather than having a single, large, and all-encompassing interface. The ISP is based on the idea that it is better to have many small interfaces that each define a specific set of methods rather than a single large interface that defines many methods.

One of the key benefits of the ISP is that it promotes a more modular and flexible design, as it allows for the creation of interfaces that are tailored to the specific needs of a client. This way, it reduces the number of unnecessary methods that a client needs to implement, and also it reduces the risk of a client depending on methods that it does not need.

An example of the ISP can be observed when creating our example messages from MessagePack or JSON files. Following the best practices, we would create an interface providing two methods, from_message_pack and from_json.

The current implementations need to implement both methods, but what if a particular class does not need to support both options? The smaller the interface, the better. The MessageParser interface will be split into two separate interfaces, each requiring the implementation of either JSON or MessagePack:

class JsonMessageParser {
public:
  virtual std::unique_ptr<Message>
  parse(const std::vector<uint8_t>& message_pack)
    const = 0;
};
class MessagePackMessageParser {
public:
  virtual std::unique_ptr<Message>
  parse(const std::vector<uint8_t>& message_pack)
    const = 0;
};

This design allows for objects derived from JsonMessageParser and MessagePackMessageParser to understand how to construct themselves from JSON and MessagePack, respectively, while preserving the independence and functionality of each function. The system remains flexible as new smaller objects can still be composed to achieve the desired functionality.

Adhering to the ISP makes the code more maintainable and less prone to errors, as it reduces the number of unnecessary methods that a client needs to implement, and it also reduces the risk of a client depending on methods that it does not need.

The Dependency inversion principle

The Dependency inversion principle is based on the idea that it is better to depend on abstractions rather than on concrete implementations, as it allows for greater flexibility and maintainability. It allows the decoupling of high-level modules from low-level modules, making them more independent and less prone to changes in the low-level modules. This way, it makes it easy to change low-level implementations without affecting high-level modules and vice versa.

The DIP can be illustrated for our messaging system if we try to use all the components via another class. Let us assume that there is a class responsible for message routing. In order to build such a class, we are going to use MessageSender as a communication module, Message based classes, and MessageSaver:

class MessageRouter {
public:
  MessageRouter(ReceiverId id)
    : id_{id} {}
  void route(const Message& message) const {
    if (message.receiver_id() == id_) {
      handler_.handle(message);
    } else {
      try {
        sender_.send(message);
      } catch (const CommunicationError& e) {
        saver_.save(message);
      }
    }
  }
private:
  const ReceiverId id_;
  const MessageHandler handler_;
  const MessageSender sender_;
  const MessageSaver saver_;
};

The new class provides only one route method, which is called once a new message is available. The router handles the message to the MessageHandler class if the message’s sender ID equals the router’s. Otherwise, the router forwards the message to the corresponding receiver. In case the delivery of the message fails and the communication layer throws an exception, the router saves the message via MessageSaver. Those messages will be delivered some other time.

The only problem is that if any dependency needs to be changed, the router’s code has to be updated accordingly. For example, if the application needs to support several types of senders (TCP and UDP), the message saver (filesystem versus remote) or message handler’s logic changes. In order to make MessageRouter agnostic to such changes, we can rewrite it using the DIP principle:

class BaseMessageHandler {
public:
    virtual ~BaseMessageHandler() {}
    virtual void handle(const Message& message) const = 0;
};
class BaseMessageSender {
public:
    virtual ~BaseMessageSender() {}
    virtual void send(const Message& message) const = 0;
};
class BaseMessageSaver {
public:
    virtual ~BaseMessageSaver() {}
    virtual void save(const Message& message) const = 0;
};
class MessageRouter {
public:
    MessageRouter(ReceiverId id,
                  const BaseMessageHandler& handler,
                  const BaseMessageSender& sender,
                  const BaseMessageSaver& saver)
        : id_{id}, handler_{handler}, sender_{sender},
          saver_{saver} {}
    void route(const Message& message) const {
        if (message.receiver_id() == id_) {
            handler_.handle(message);
        } else {
            try {
                sender_.send(message);
            } catch (const CommunicationError& e) {
                saver_.save(message);
            }
        }
    }
private:
    ReceiverId id_;
    const BaseMessageHandler& handler_;
    const BaseMessageSender& sender_;
    const BaseMessageSaver& saver_;
};
int main() {
  auto id      = ReceiverId{42};
  auto handler = MessageHandler{};
  auto sender = MessageSender{
    Communication::get_instance()};
  auto saver =
    FilesystemMessageSaver{"/tmp/undelivered_messages"};
  auto router = MessageRouter{id, sender, saver};
}

In this revised version of the code, MessageRouter is now decoupled from specific implementations of the message handling, sending, and saving logic. Instead, it relies on abstractions represented by BaseMessageHandler, BaseMessageSender, and BaseMessageSaver. This way, any class that derives from these base classes can be used with MessageRouter, which makes the code more flexible and easier to extend in the future. The router is not concerned with the specifics of how messages are handled, sent, or saved – it only needs to know that these operations can be performed.

Adhering to the DIP makes code more maintainable and less prone to errors. It decouples high-level modules from low-level modules, making them more independent and less prone to changes in low-level modules. It also allows for greater flexibility, making it easy to change low-level implementations without affecting high-level modules and vice versa. Later in this book, dependency inversion will help us mock parts of the system while developing unit tests.

The KISS principle

The KISS principle, which stands for “Keep It Simple, Stupid,” is a design philosophy that emphasizes the importance of keeping things simple and straightforward. This principle is particularly relevant in the world of programming, where complex code can lead to bugs, confusion, and slow development time.

Here are some examples of how the KISS principle can be applied in C++:

  • Avoid Overcomplicating Code: In C++, it’s easy to get carried away with complex algorithms, data structures, and design patterns. However, these advanced techniques can lead to code that is harder to understand and debug. Instead, try to simplify the code as much as possible. For example, using a simple for loop instead of a complex algorithm can often be just as effective and much easier to understand.
  • Keep Functions Small: Functions in C++ should be small, focused, and easy to understand. Complex functions can quickly become difficult to maintain and debug, so try to keep functions as simple and concise as possible. A good rule of thumb is to aim for functions that are no longer than 30-50 lines of code.
  • Use Clear and Concise Variable Names: In C++, variable names play a crucial role in making code readable and understandable. Avoid using abbreviations and instead opt for clear and concise names that accurately describe the purpose of the variable.
  • Avoid Deep Nesting: Nested loops and conditional statements can make code hard to read and follow. Try to keep the nesting levels as shallow as possible, and consider breaking up complex functions into smaller, simpler functions.
  • Write Simple, Readable Code: Above all, aim to write code that is easy to understand and follow. This means using clear and concise language and avoiding complicated expressions and structures. Code that is simple and easy to follow is much more likely to be maintainable and bug-free.
  • Avoid Complex Inheritance Hierarchy: Complex inheritance hierarchies can make code more difficult to understand, debug, and maintain. The more complex the inheritance structure, the harder it becomes to keep track of the relationships between classes and determine how changes will affect the rest of the code.

In conclusion, the KISS principle is a simple and straightforward design philosophy that can help developers write clear, concise, and maintainable code. By keeping things simple, developers can avoid bugs and confusion and speed up development time.

The KISS and SOLID Principles together

The SOLID principles and the KISS principle are both important design philosophies in software development, but they can sometimes contradict each other.

The SOLID principles are a set of five principles that guide the design of software, aimed at making it more maintainable, scalable, and flexible. They focus on creating a clean, modular architecture that follows good object-oriented design practices.

The KISS principle, on the other hand, is all about keeping things simple. It advocates for straightforward, simple solutions, avoiding complex algorithms and structures that can make code hard to understand and maintain.

While both SOLID and KISS aim to improve software quality, they can sometimes be at odds. For example, following the SOLID principles may result in code that is more complex and harder to understand to achieve greater modularity and maintainability. Similarly, the KISS principle may result in less flexible and scalable code to keep it simple and straightforward.

In practice, developers often have to strike a balance between the SOLID principles and the KISS principle. On the one hand, they want to write code that is maintainable, scalable, and flexible. On the other hand, they want to write code that is simple and easy to understand. Finding this balance requires careful consideration of trade-offs and an understanding of when each approach is most appropriate.

When I have to choose between the SOLID and KISS approaches, I think about something my boss, Amir Taya, said, “When building a Ferrari, you need to start from a scooter.” This phrase is an exaggerated example of KISS: if you do not know how to build a feature, make the simplest working version (KISS), re-iterate, and extend the solution using SOLID principles if needed.

Side effects and immutability

Side effects and immutability are two important concepts in programming that have a significant impact on the quality and maintainability of code.

Side effects refer to changes that occur in the state of the program as a result of executing a particular function or piece of code. Side effects can be explicit, such as writing data to a file or updating a variable, or implicit, such as modifying the global state or causing unexpected behavior in other parts of the code.

Immutability, on the other hand, refers to the property of a variable or data structure that cannot be modified after it has been created. In functional programming, immutability is achieved by making data structures and variables constant and avoiding side effects.

The importance of avoiding side effects and using immutable variables lies in the fact that they make code easier to understand, debug, and maintain. When code has few side effects, it is easier to reason about what it does and what it does not do. This makes finding and fixing bugs and making changes to the code easier without affecting other parts of the system.

In contrast, code with many side effects is harder to understand, as the state of the program can change in unexpected ways. This makes it more difficult to debug and maintain and can lead to bugs and unexpected behavior.

Functional programming languages have long emphasized the use of immutability and the avoidance of side effects, but it is now possible to write code with these properties using C++. The easiest way to achieve it is to follow the C++ Core Guidelines for Constants and Immutability.

Con.1 – by default, make objects immutable

You can declare a built-in data type or an instance of a user-defined data type as constant, resulting in the same effect. Attempting to modify it will result in a compiler error:

struct Data {
  int val{42};
};
int main() {
  const Data data;
  data.val = 43; // assignment of member 'Data::val' in
                 // read-only object
  const int val{42};
  val = 43; // assignment of read-only variable 'val'
}

The same applies to loops:

for (const int i : array) {
  std::cout << i << std::endl; // just reading: const
}
for (int i : array) {
  std::cout << i << std::endl; // just reading: non-const
}

This approach allows the prevention of hard-to-notice changes of value.

Probably, the only exception is function parameters passed by value:

void foo(const int value);

Such parameters are rarely passed as const and rarely mutated. In order to avoid confusion, it is recommended not to enforce this rule in such cases.

Con.2 – by default, make member functions const

A member function (method) shall be marked as const unless it changes the observable state of an object. The reason behind this is to give a more precise statement of design intent, better readability, maintainability, more errors caught by the compiler, and theoretically more optimization opportunities:

class Book {
public:
  std::string name() { return name_; }
private:
  std::string name_;
};
void print(const Book& book) {
  cout << book.name()
       << endl; // ERROR: 'this' argument to member
                // function
                // 'name' has type 'const Book', but
                // function is not marked
                // const clang(member_function_call_bad_cvr)
}

There are two types of constness: physical and logical:

Physical constness: An object is declared const and cannot be changed.

Logical constness: An object is declared const but can be changed.

Logical constness can be achieved with the mutable keyword. In general, it is a rare use case. The only good example I can think of is storing in an internal cache or using a mutex:

class DataReader {
public:
  Data read() const {
    auto lock = std::lock_guard<std::mutex>(mutex);
    // read data
    return Data{};
  }
private:
  mutable std::mutex mutex;
};

In this example, we need to change the mutex variable to lock it, but this does not affect the logical constness of the object.

Please be aware that there exist legacy codes/libraries that provide functions that declare T*, despite not making any changes to the T. This presents an issue for individuals who are trying to mark all logically constant methods as const. In order to enforce constness, you can do the following:

  • Update the library/code to be const-correct, which is the preferred solution.
  • Provide a wrapper function casting away the constness.

Example

void read_data(int* data); // Legacy code: read_data does
                           // not modify `*data`
void read_data(const int* data) {
  read_data(const_cast<int*>(data));
}

Note that this solution is a patch that can be used only when the declaration of read_data cannot be modified.

Con.3 – by default, pass pointers and references to const

This one is easy; it is far easier to reason about programs when called functions do not modify state.

Let us look at the two following functions:

void foo(char* p);
void bar(const char* p);

Does the foo function modify the data the p pointer points to? We cannot answer by looking at the declaration, so we assume it does by default. However, the bar function states explicitly that the content of p will not be changed.

Con.4 – use const to define objects with values that do not change after construction

This rule is very similar to the first one, enforcing the constness of objects that are not expected to be changed in the future. It is often helpful with classes such as Config that are created at the beginning of the application and not changed during its lifetime:

class Config {
public:
  std::string hostname() const;
  uint16_t port() const;
};
int main(int argc, char* argv[]) {
  const Config config = parse_args(argc, argv);
  run(config);
}

Con.5 – use constexpr for values that can be computed at compile time

Declaring variables as constexpr is preferred over const if the value is computed at compile time. It provides such benefits as better performance, better compile-time checking, guaranteed compile-time evaluation, and no possibility of race conditions.

Constness and data races

Data races occur when multiple threads access a shared variable simultaneously, and at least one tries to modify it. There are synchronization primitives such as mutexes, critical sections, spinlocks, and semaphores, allowing the prevention of data races. The problem with these primitives is that they either do expensive system calls or overuse the CPU, which makes the code less efficient. However, if none of the threads modifies the variable, there is no place for data races. We learned that constexpr is thread-safe (does not need synchronization) because it is defined at compile time. What about const? It can be thread-safe under the below conditions.

The variable has been const since its creation. If a thread has direct or indirect (via a pointer or reference) non-const access to the variable, all the readers need to use mutexes. The following code snippet illustrates constant and non-constant access from multiple threads:

void a() {
  auto value = int{42};
  auto t = std::thread([&]() { std::cout << value; });
  t.join();
}
void b() {
  auto value = int{42};
  auto t = std::thread([&value = std::as_const(value)]() {
    std::cout << value;
  });
  t.join();
}
void c() {
  const auto value = int{42};
  auto t = std::thread([&]() {
      auto v = const_cast<int&>(value);
      std::cout << v;
  });
  t.join();
}
void d() {
  const auto value = int{42};
  auto t = std::thread([&]() { std::cout << value; });
  t.join();
}

In the a function, the value variable is owned as non-constant by both the main thread and t, which makes the code potentially not thread-safe (if a developer decides to change the value later in the main thread). In the b, the main thread has “write” access to value while t receives it via a const reference, but still, it is not thread-safe. The c function is an example of very bad code: the value is created as a constant in the main thread and passed as a const reference but then the constness is cast away, which makes this function not thread-safe. Only the d function is thread-safe because neither the main thread nor t can modify the variable.

The data type and all sub-types of the variable are either physically constant or their logical constness implementation is thread-safe. For example, in the following example, the Point struct is physically constant because its x and y field members are primitive integers, and both threads have only const access to it:

struct Point {
  int x;
  int y;
};
void foo() {
  const auto point = Point{.x = 10, .y = 10};
  auto t           = std::thread([&]() { std::cout <<
    point.x; });
  t.join();
}

The DataReader class that we saw earlier is logically constant because it has a mutable variable, mutex, but this implementation is also thread-safe (due to the lock):

class DataReader {
public:
  Data read() const {
    auto lock = std::lock_guard<std::mutex>(mutex);
    // read data
    return Data{};
  }
private:
  mutable std::mutex mutex;
};

However, let us look into the following case. The RequestProcessor class processes some heavy requests and caches the results in an internal variable:

class RequestProcessor {
public:
  Result process(uint64_t request_id,
                 Request request) const {
    if (auto it = cache_.find(request_id); it !=
      cache_.cend()) {
      return it->second;
    }
    // process request
    // create result
    auto result = Result{};
    cache_[request_id] = result;
    return result;
  }
private:
  mutable std::unordered_map<uint64_t, Result> cache_;
};
void process_request() {
  auto requests = std::vector<std::tuple<uint64_t,
    Request>>{};
  const auto processor = RequestProcessor{};
  for (const auto& request : requests) {
    auto t = std::thread([&]() {
      processor.process(std::get<0>(request),
                        std::get<1>(request));
    });
    t.detach();
  }
}

This class is logically safe, but the cache_ variable is changed in a non-thread-safe way, which makes the class non-thread-safe even when declared as const.

Note that when working with STL containers, it is essential to remember that, despite current implementations tending to be thread-safe (physically and logically), the standard provides very specific thread-safety guarantees.

All functions in a container can be called simultaneously by various threads on different containers. Broadly, functions from the C++ standard library don’t read objects accessible to other threads unless they are reachable through the function arguments, which includes the this pointer.

All const member functions are thread-safe, meaning they can be invoked simultaneously by various threads on the same container. Furthermore, the begin(), end(), rbegin(), rend(), front(), back(), data(), find(), lower_bound(), upper_bound(), equal_range(), at(), and operator[] (except in associative containers) member functions also behave as const with regard to thread safety. In other words, they can also be invoked by various threads on the same container. Broadly, C++ standard library functions won’t modify objects unless those objects are reachable, directly or indirectly, via the function’s non-const arguments, which includes the this pointer.

Different elements in the same container can be altered simultaneously by different threads, with the exception of std::vector<bool> elements. For example, a std::vector of std::future objects can receive values from multiple threads at once.

Operations on iterators, such as incrementing an iterator, read the underlying container but don’t modify it. These operations can be performed concurrently with operations on other iterators of the same container, with the const member functions, or with reads from the elements. However, operations that invalidate any iterators modify the container and must not be performed concurrently with any operations on existing iterators, even those that are not invalidated.

Elements of the same container can be altered concurrently with those member functions that don’t access these elements. Broadly, C++ standard library functions won’t read objects indirectly accessible through their arguments (including other elements of a container) except when required by its specification.

Lastly, operations on containers (as well as algorithms or other C++ standard library functions) can be internally parallelized as long as the user-visible results remain unaffected. For example, std::transform can be parallelized, but std::for_each cannot, as it is specified to visit each element of a sequence in order.

The idea of having a single mutable reference to an object became one of the pillars of the Rust programming language. This rule is in place to prevent data races, which occur when multiple threads access the same mutable data concurrently, resulting in unpredictable behavior and potential crashes. By allowing only one mutable reference to an object at a time, Rust ensures that concurrent access to the same data is properly synchronized and avoids data races.

In addition, this rule helps prevent mutable aliasing, which occurs when multiple mutable references to the same data exist simultaneously. Mutable aliasing can lead to subtle bugs and make code difficult to reason about, especially in large and complex code bases. By allowing only one mutable reference to an object, Rust avoids mutable aliasing and helps ensure that code is correct and easy to understand.

However, it’s worth noting that Rust also allows multiple immutable references to an object, which can be useful in scenarios where concurrent access is necessary but mutations are not. By allowing multiple immutable references, Rust can provide better performance and concurrency while still maintaining safety and correctness.

Summary

In this chapter, we covered the SOLID principles, the KISS principle, constness, and immutability. Let’s see what you learned!

  • SOLID principles: SOLID is a set of five principles that help us create code that’s easy to maintain, scalable, and flexible. By understanding these principles, you’re on your way to designing code that’s a dream to work with!
  • The KISS principle: The KISS principle is all about keeping things simple. By following this principle, you’ll avoid overcomplicating your code, making it easier to maintain and debug.
  • Constness: Constness is a property in C++ that makes objects read-only. By declaring objects as const, you can ensure that their values won’t be accidentally changed, making your code more stable and predictable.
  • Immutability: Immutability is all about making sure objects can’t be changed after their creation. By making objects immutable, you can avoid sneaky bugs and make your code more predictable.

With these design principles under your belt, you’re on your way to writing code that’s both robust and reliable. Happy coding!

In the next chapter, we will try to understand what causes bad code.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Enrich your coding skills using features from the modern C++ standard and industry approved libraries
  • Implement refactoring techniques and SOLID principles in C++
  • Apply automated tools to improve your code quality
  • Purchase of the print or Kindle book includes a free PDF eBook

Description

Despite the prevalence of higher-level languages, C++ is still running the world, from bare-metal embedded systems to distributed cloud-native systems. C++ is on the frontline whenever there is a need for a performance-sensitive tool supporting complex data structures. The language has been actively evolving for the last two decades. This book is a comprehensive guide that shows you how to implement SOLID principles and refactor legacy code using the modern features and approaches of C++, the standard library, Boost library collection, and Guidelines Support Library by Microsoft. The book begins by describing the essential elements of writing clean code and discussing object-oriented programming in C++. You’ll explore the design principles of software testing with examples of using popular unit testing frameworks such as Google Test. The book also guides you through applying automated tools for static and dynamic code analysis using Clang Tools. By the end of this book, you’ll be proficient in applying industry-approved coding practices to design clean, sustainable, and readable real-world C++ code.

Who is this book for?

This book will benefit experienced C++ programmers the most, but is also suitable for technical leaders, software architects, and senior software engineers who want to save on costs and improve software development process efficiency by using modern C++ features and automated tools.

What you will learn

  • Leverage the rich type system of C++ to write safe and elegant code
  • Create advanced object-oriented designs using the unique features of C++
  • Minimize code duplication by using metaprogramming
  • Refactor code safely with the help of unit tests
  • Ensure code conventions and format with clang-format
  • Facilitate the usage of modern features automatically with clang-tidy
  • Catch complex bugs such as memory leakage and data races with Clang AddressSanitizer and ThreadSanitizer
Estimated delivery fee Deliver to South Africa

Standard delivery 10 - 13 business days

$12.95

Premium delivery 3 - 6 business days

$34.95
(Includes tracking information)

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Jul 19, 2024
Length: 368 pages
Edition : 1st
Language : English
ISBN-13 : 9781837633777
Category :
Languages :

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
OR
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Estimated delivery fee Deliver to South Africa

Standard delivery 10 - 13 business days

$12.95

Premium delivery 3 - 6 business days

$34.95
(Includes tracking information)

Product Details

Publication date : Jul 19, 2024
Length: 368 pages
Edition : 1st
Language : English
ISBN-13 : 9781837633777
Category :
Languages :

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
$199.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts
$279.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total $ 93.95 134.97 41.02 saved
Refactoring with C++
$27.98 $39.99
Modern CMake for C++
$34.98 $49.99
Data Structures and Algorithms with the C++ STL
$30.99 $44.99
Total $ 93.95 134.97 41.02 saved Stars icon
Banner background image

Table of Contents

17 Chapters
Chapter 1: Coding Standards in C++ Chevron down icon Chevron up icon
Chapter 2: Main Software Development Principles Chevron down icon Chevron up icon
Chapter 3: Causes of Bad Code Chevron down icon Chevron up icon
Chapter 4: Identifying Ideal Candidates for Rewriting – Patterns and Anti-Patterns Chevron down icon Chevron up icon
Chapter 5: The Significance of Naming Chevron down icon Chevron up icon
Chapter 6: Utilizing a Rich Static Type System in C++ Chevron down icon Chevron up icon
Chapter 7: Classes, Objects, and OOP in C++ Chevron down icon Chevron up icon
Chapter 8: Designing and Developing APIs in C++ Chevron down icon Chevron up icon
Chapter 9: Code Formatting and Naming Conventions Chevron down icon Chevron up icon
Chapter 10: Introduction to Static Analysis in C++ Chevron down icon Chevron up icon
Chapter 11: Dynamic Analysis Chevron down icon Chevron up icon
Chapter 12: Testing Chevron down icon Chevron up icon
Chapter 13: Modern Approach to Managing Third Parties Chevron down icon Chevron up icon
Chapter 14: Version Control Chevron down icon Chevron up icon
Chapter 15: Code Review Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Rating distribution
Full star icon Full star icon Full star icon Full star icon Full star icon 5
(1 Ratings)
5 star 100%
4 star 0%
3 star 0%
2 star 0%
1 star 0%
Nathaniel Ryan Doromal Sep 19, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Here's my review of "Refactoring with C++" by Dmitry Danilov. This book is a unique and excellent resource for C++ improvers, filling a crucial gap in the C++ literature with its focus on refactoring specifically for C++.Martin Fowler's work on refactoring was an important contribution to the software engineering literature and a definite must-read. However, it focuses on Java and heavily object-oriented patterns. I hadn't seen a detailed topic of refactoring in the C++ domain until I came across Danilov's book.C++ refactoring looks and feels quite different from Java refactoring. There are more considerations and possible ways to proceed. The patterns presented in this book are good to consider as the best patterns for refactoring in C++.If I were to offer some minor criticisms of the book, it would be that it's not entirely tailored for beginners. The difficulty of the content doesn't always increase gradually, and there are intermittent advanced topics (such as concurrency) that appear unexpectedly. However, the descriptions of best software engineering practices are well-summarized and presented in a way that even beginners can benefit from.For those learning C++, I recommend studying the examples very carefully to understand the patterns presented.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact [email protected] with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at [email protected] using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on [email protected] with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on [email protected] within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on [email protected] who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on [email protected] within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela