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

для правильной обработки копирования объектов эмпирическим правилом является правило трех. С 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;
};

2 ответов


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

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

  • копирующий конструктор
  • оператор присваивания
  • переместить конструктор
  • деструктор
  • функция подкачки

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

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

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

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

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

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

функции половинку?

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

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

так swap метод.

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

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


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

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

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

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

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

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

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

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