what are they and why are they needed?

Rule of three

Scott Meyers one of the C++ gurus and author of excellent books, first formulated the rule of three in his books “Effective C++”.

The rule of three which states that if a class needs one of the following three methods, then it will most likely need the other two as well:

1. Destructor is a special class method that is automatically called when an object is destroyed. The Destructor is that action movie character who removes all traces of the operation before leaving. It ensures that all allocated resources are gracefully released when the object is no longer needed.

2. Copy constructor allows you to create new objects as copies of existing ones. That is, when you copy an object, every bit of information from the original is transferred to the new object. Without an explicitly defined copy constructor, C++ will provide a standard one that will copy all the fields of your object. This is not very good if the object manages an external resource, for example, allocates memory. Why? Because now two objects will think that they own the same resource and will try to free it when destroyed.

3. Copy assignment operator allows one already existing object to assume the state of another existing object. If this operator is not explicitly defined, C++ will generate it for you, but as with the copy constructor, this can lead to resource management problems.

So: the rule of three did not arise out of nowhere due to some requirements for memory and resource management in C++, where careless handling of dynamically allocated memory, file descriptors or network connections can easily lead to resource leaks or even crash the program.

Let's look at an example of a class that Not follows the rule of three:

class BrokenResource {
public:
    char* data;

    BrokenResource(const char* input) {
        data = new char[strlen(input) + 1];
        strcpy(data, input);
    }

    ~BrokenResource() {
        delete[] data;
    }
    // конструктор копирования и оператор присваивания не определены!
};

When copying an object BrokenResource the compiler will generate a default copy constructor and assignment operator that simply copies the pointer dataleading to a potential double free.

Let's fix the previous example by explicitly implementing the rule of three:

class FixedResource {
public:
    char* data;

    FixedResource(const char* input) {
        data = new char[strlen(input) + 1];
        strcpy(data, input);
    }

    ~FixedResource() {
        delete[] data;
    }

    // конструктор копирования
    FixedResource(const FixedResource& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    // оператор присваивания
    FixedResource& operator=(const FixedResource& other) {
        if (this != &other) { // предотвращение самоприсваивания
            delete[] data; // освобождаем существующий ресурс
            data = new char[strlen(other.data) + 1];
            strcpy(data, other.data);
        }
        return *this;
    }
};

Added a copy constructor and assignment operator to ensure that each object has its own copy of the data.

Rule of Five

Rule of Five in C++ is an evolution of the previous rule of three, adapted to accommodate innovations C++11such as movement semantics. This rule states that if you explicitly define one of the following five special methods of a class, you most likely need to explicitly define the other four:

  1. Destructor.

  2. Copy constructor.

  3. Copy assignment operator.

  4. Move constructor.

  5. Move Assignment Operator.

The Rule of Five, also known as the Rule of Five, addresses issues of efficiency and safety when working with resources. Copying resources can be performance-intensive, but moving avoids unnecessary copy operations by transferring ownership of the resources directly to the new object.

Let's look at an anti-example:

class ResourceHolder {
public:
    int* data;

    ResourceHolder(int value) : data(new int(value)) {}
    ~ResourceHolder() { delete data; }
    // Правило пяти не соблюдено: отсутствуют конструктор копирования,
    // оператор присваивания копированием, конструктор перемещения и
    // оператор присваивания перемещением.
};

This class manages dynamically allocated memory, but only defines a constructor and destructor. Without explicitly defining copy and move operations, the compiler will generate them automatically, often leading to double frees

Compliance:

class ProperResource {
public:
    int* data;

    ProperResource(int value) : data(new int(value)) {}
    ~ProperResource() { delete data; }

    // конструктор копирования
    ProperResource(const ProperResource& other) : data(new int(*other.data)) {}

    // оператор присваивания копированием
    ProperResource& operator=(const ProperResource& other) {
        if (this != &other) {
            *data = *other.data;
        }
        return *this;
    }

    // конструктор перемещения
    ProperResource(ProperResource&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }

    // оператор присваивания перемещением
    ProperResource& operator=(ProperResource&& other) noexcept {
        if (this != &other) {
            delete data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
};

IN ProperResource All five special methods that control copying and moving resources are explicitly defined.

Smart pointers

It was impossible not to talk about them in the context of this article.

Smart pointers automatically manage memory, making code cleaner.

C++ offers several types of smart pointers, but let's pay attention to the two main ones: std::unique_ptr And std::shared_ptr.

std::unique_ptr provides exclusive ownership of the resource. This means there can't be two std::unique_ptr, pointing to the same object. When it is destroyed, the resource is also destroyed.

#include <memory>

void useUniquePtr() {
    std::unique_ptr<int> ptr(new int(10)); // инициализация с новым int
    // нет необходимости вызывать delete, уничтожение ptr автоматически освободит память
}

std::shared_ptr supports shared resource ownership through reference counting. The resource will only be released when the last std::shared_ptrThe owner of this resource will be destroyed or discarded.

#include <memory>

void useSharedPtr() {
    std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(20);
    std::shared_ptr<int> sharedPtr2 = sharedPtr1; // оба указателя сейчас владеют ресурсом.

    // ресурс будет автоматически освобожден после уничтожения последнего shared_ptr.
}

One of the most common mistakes is completely ignoring the need to implement these methods in classes that manage resources. This can lead to memory leaks, double frees, and other problems. Even when implementing these methods, it is easy to make the mistake of not correctly copying or moving resources, which will lead to similar problems.

In modern C++, everyone uses smart pointers and other methods that independently manage their resources, the class may not require explicit implementation of these five methods at all. This is known as zero rule.

But we shouldn’t forget about these rules either; they contributed to the development of the C++ language.

C++ has many options for solving a problem, which will often differ in terms of performance and flexibility. One of these possibilities is the semantics of copying and moving. OTUS experts will talk about how they differ syntactically and what optimization opportunities this opens up for us at free webinar. Registration for the webinar available via link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *