Что такое правило четырех (с половиной)?

Для правильной работы с объектным копированием эмпирическое правило - это правило трех. В C++11 семантика перемещения - это вещь, так что вместо этого это правило пяти. Тем не менее, в дискуссиях здесь и в Интернете я также видел ссылки на Правило Четырех (с половиной), которое является комбинацией Правил Пяти и идиомы копирования и обмена.

Так что же такое Правило Четырех (с половиной)? Какие функции необходимо реализовать и как должно выглядеть тело каждой функции? Какая функция является половиной? Есть ли какие-либо недостатки или предупреждения для этого подхода по сравнению с правилом пяти?

Вот эталонная реализация, которая напоминает мой текущий код. Если это неверно, как будет выглядеть правильная реализация?

//I understand that in this example, I could just use `std::unique_ptr`.
//Just assume it's a more complex resource.
#include <utility>

class Foo {
public:
    //We must have a default constructor so we can swap during copy construction.
    //It need not be useful, but it should be swappable and deconstructable.
    //It can be private, if it's not truly a valid state for the object.
    Foo() : resource(nullptr) {}

    //Normal constructor, acquire resource
    Foo(int value) : resource(new int(value)) {}

    //Copy constructor
    Foo(Foo const& other) {
        //Copy the resource here.
        resource = new int(*other.resource);
    }

    //Move constructor
    //Delegates to default constructor to put us in safe state.
    Foo(Foo&& other) : Foo() {
        swap(other);
    }

    //Assignment
    Foo& operator=(Foo other) {
        swap(other);
        return *this;
    }

    //Destructor
    ~Foo() {
        //Free the resource here.
        //We must handle the default state that can appear from the copy ctor.
        //(The if is not technically needed here. `delete nullptr` is safe.)
        if (resource != nullptr) delete resource;
    }

    //Swap
    void swap(Foo& other) {
        using std::swap;

        //Swap the resource between instances here.
        swap(resource, other.resource);
    }

    //Swap for ADL
    friend void swap(Foo& left, Foo& right) {
        left.swap(right);
    }

private:
    int* resource;
};

3 ответа

Решение

Так что же такое Правило Четырех (с половиной)?

"Правило Большой четверки (с половиной)" гласит, что если вы реализуете один из

  • Конструктор копирования
  • Оператор присваивания
  • Конструктор перемещения
  • Деструктор
  • Функция обмена

тогда вы должны иметь политику в отношении других.

Какие функции нужно реализовать и как должно выглядеть тело каждой функции?

  • конструктор по умолчанию (который может быть закрытым)
  • конструктор копирования (здесь у вас есть реальный код для обработки вашего ресурса)
  • Переместить конструктор (используя конструктор по умолчанию и swap):

    S(S&& s) : S{} { swap(*this, s); }
    
  • оператор присваивания (используя конструктор и своп)

    S& operator=(S s) { swap(*this, s); }
    
  • деструктор (глубокая копия вашего ресурса)

  • замена друга (не имеет реализации по умолчанию:/ вы, вероятно, захотите поменять местами каждого участника). Это важно в отличие от метода члена подкачки: std::swap использует конструктор перемещения (или копирования), который приведет к бесконечной рекурсии.

Какая функция является половиной?

Из предыдущей статьи:

"Чтобы реализовать идиому Copy-Swap, ваш класс управления ресурсами должен также реализовать функцию swap () для выполнения перестановки член-член (есть ваше"… (полтора) ")"

Итак swap метод.

Есть ли какие-либо недостатки или предупреждения для этого подхода по сравнению с правилом пяти?

Предупреждение, которое я уже написал, собирается написать правильный своп, чтобы избежать бесконечной рекурсии.

Есть ли какие-либо недостатки или предупреждения для этого подхода по сравнению с правилом пяти?

Хотя это может сэкономить дублирование кода, использование копирования и замены просто приводит к худшим классам, если быть грубым. Вы снижаете производительность своего класса, в том числе назначение заданий (если вы используете унифицированный оператор присваивания, которым я тоже не фанат), что должно быть очень быстрым. Взамен вы получаете гарантию сильного исключения, которая на первый взгляд кажется хорошей. Дело в том, что вы можете получить гарантию сильного исключения из любого класса с помощью простой универсальной функции:

template <class T>
void copy_and_swap(T& target, T source) {
    using std::swap;
    swap(target, std::move(source));
}

И это все. Таким образом, люди, которые нуждаются в строгой безопасности исключений, могут получить это так или иначе. И, честно говоря, сильное исключение безопасности в любом случае является довольно нишевым.

Реальный способ сохранить дублирование кода - через правило нуля: выбирайте переменные-члены, чтобы вам не нужно было писать какие-либо специальные функции. В реальной жизни я бы сказал, что в 90+ % случаев, когда я вижу специальные функции-члены, их можно было бы легко избежать. Даже если у вашего класса действительно есть какая-то особая логика, необходимая для специальной функции-члена, вам, как правило, лучше поместить ее в член. Вашему классу логгера может потребоваться очистить буфер в его деструкторе, но это не причина писать деструктор: напишите небольшой класс буфера, который обрабатывает сброс, и используйте его в качестве члена вашего логгера. У регистраторов потенциально есть все виды других ресурсов, которые могут обрабатываться автоматически, и вы хотите, чтобы компилятор автоматически генерировал код для копирования / перемещения / уничтожения.

Особенность C++ заключается в том, что автоматическая генерация специальных функций - это все или ничего для каждой функции. То есть конструктор копирования (например) либо генерируется автоматически с учетом всех членов, либо вы должны написать (и, что еще хуже, сохранить) все вручную. Так что это сильно подталкивает вас к тому, чтобы толкать вещи вниз.

В тех случаях, когда вы пишете класс для управления ресурсом и вам необходимо с этим справиться, он, как правило, должен быть: а) относительно небольшим и б) относительно общим / многократно используемым. Первое означает, что немного дублированного кода не имеет большого значения, а второе означает, что вы, вероятно, не хотите оставлять производительность на столе.

В итоге я настоятельно не рекомендую использовать копирование и обмен, а также использовать унифицированные операторы присваивания. Попробуйте следовать правилу нуля, если не можете, следуйте правилу пяти. Написать swap только если вы можете сделать это быстрее, чем общий обмен (который делает 3 хода), но обычно вам не нужно беспокоиться.

Проще говоря, просто запомните это.

Правило 0 :

Classes have neither custom destructors, copy/move constructors or copy/move assignment operators.

Правило 3. Если вы реализуете индивидуальную версию любого из них, вы реализуете их все.

Destructor, Copy constructor, copy assignment

Правило 5. Если вы реализуете настраиваемый конструктор перемещения или оператор присваивания перемещения, вам необходимо определить все 5 из них. Требуется для семантики перемещения.

Destructor, Copy constructor, copy assignment, move constructor, move assignment

Правило четырех с половиной : То же, что Правило 5, но с идиомой копировать и менять местами. С включением метода подкачки присваивание копирования и присваивание перемещения объединяются в один оператор присваивания.

Destructor, Copy constructor, move constructor, assignment, swap (the half part)

      Destructor: ~Class();
Copy constructor: Class(Class &);
Move constructor: Class(Class &&);
Assignment: Class & operator = (Class);
Swap: void swap(Class &);

Предупреждений нет, преимущество в том, что он быстрее присваивается, поскольку передача по значению копии на самом деле более эффективна, чем создание временного объекта в теле метода.

И теперь, когда у нас есть этот временный объект, мы просто выполняем замену временного объекта. Он автоматически уничтожается, когда выходит за пределы области видимости, и теперь у нас есть значение из правой части оператора в нашем объекте.

Ссылки :

https://www.linkedin.com/learning/c-plus-plus-advanced-topics/rule-of-five?u=67551194https://en.cppreference.com/w/cpp/language/rule_of_three

Другие вопросы по тегам