Что такое идиома копирования и обмена?

poate iti merge asta

static int clearCacheFolder(final File dir, final int numDays) {

        int deletedFiles = 0;
        if (dir!= null && dir.isDirectory()) {
            try {
                for (File child:dir.listFiles()) {

                    //first delete subdirectories recursively
                    if (child.isDirectory()) {
                        deletedFiles += clearCacheFolder(child, numDays);
                    }

                    //then delete the files and subdirectories in this dir
                    //only empty directories can be deleted, so subdirs have been done first
                    if (child.lastModified() < new Date().getTime() - numDays * DateUtils.DAY_IN_MILLIS) {
                        if (child.delete()) {
                            deletedFiles++;
                        }
                    }
                }
            }
            catch(Exception e) {
                Log.e("ATTENTION!", String.format("Failed to clean the cache, error %s", e.getMessage()));
            }
        }
        return deletedFiles;
    }

    public static void clearCache(final Context context, final int numDays) {
        Log.i("ADVL", String.format("Starting cache prune, deleting files older than %d days", numDays));
        int numDeletedFiles = clearCacheFolder(context.getCacheDir(), numDays);
        Log.i("ADVL", String.format("Cache pruning completed, %d files deleted", numDeletedFiles));
    }
1858
задан Community 23 May 2017 в 11:55
поделиться

2 ответа

Обзор

Зачем нам нужна идиома копирования и обмена?

Любой класс, который управляет ресурсом ( оболочка , как интеллектуальный указатель) необходимо реализовать Большая тройка . Хотя цели и реализация копирующего конструктора и деструктора просты, оператор копирования-присваивания, возможно, является наиболее тонким и сложным. Как это сделать? Каких ловушек следует избегать?

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

Как это работает?

Концептуально он работает, используя функциональные возможности конструктора копирования для создания локальной копии данных, а затем берет скопированные данные с помощью функции swap , замена старых данных новыми. Затем временная копия разрушается, забирая с собой старые данные. У нас осталась копия новых данных.

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

Функция подкачки - это не вызывающая функция, которая меняет местами два объекта класса, член за членом.У нас может возникнуть соблазн использовать std :: swap вместо предоставления собственного, но это невозможно; std :: swap использует конструктор копирования и оператор присваивания копии в своей реализации, и в конечном итоге мы попытаемся определить оператор присваивания в терминах самого себя!

(Не только это, но и неквалифицированные вызовы swap будут использовать наш пользовательский оператор swap, пропуская ненужное построение и разрушение нашего класса, которое повлечет за собой std :: swap . )


Подробное объяснение

Цель

Рассмотрим конкретный случай. Мы хотим управлять динамическим массивом в бесполезном в противном случае классе. Начнем с рабочего конструктора, конструктора копирования и деструктора:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Этот класс почти успешно управляет массивом, но для правильной работы ему требуется operator = .

Неудачное решение

Вот как может выглядеть наивная реализация:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

И мы говорим, что закончили; это теперь управляет массивом без утечек. Однако он страдает от трех проблем, последовательно отмеченных в коде как (n) .

  1. Первый - это тест самостоятельного назначения. Эта проверка служит двум целям: это простой способ предотвратить запуск ненужного кода при самостоятельном назначении и защищает нас от мелких ошибок (таких как удаление массива только для того, чтобы попытаться скопировать его). Но во всех остальных случаях это просто служит для замедления программы и действует как шум в коде; Самостоятельное присвоение происходит редко, поэтому в большинстве случаев эта проверка является пустой тратой. Было бы лучше, если бы оператор мог нормально работать и без него.

  2. Во-вторых, он обеспечивает только базовую гарантию исключения. Если новый int [mSize] завершится ошибкой, * this будет изменен. (А именно, размер неправильный и данные пропали!) Для надежной гарантии исключения это должно быть что-то вроде:

     dumb_array & operator = (const dumb_array & other)
    {
    if (this! = & other) // (1)
     {
     // готовим новые данные перед заменой старых
    std :: size_t newSize = other.mSize;
    int * newArray = newSize? новый интервал [новый размер] (): nullptr; // (3)
    std :: copy (other.mArray, other.mArray + newSize, newArray); // (3)
    
     // заменяем старые данные (все не выбрасываются)
    удалить [] mArray;
    mSize = newSize;
    mArray = newArray;
     }
    
    вернуть * это;
    }
    
  3. Код расширился! Это приводит нас к третьей проблеме: дублированию кода. Наш оператор присваивания эффективно дублирует весь код, который мы уже написали где-то еще, и это ужасно.

В нашем случае ядро ​​составляет всего две строки (выделение и копия), но с более сложными ресурсами это раздувание кода может стать серьезной проблемой. Мы должны стремиться никогда не повторяться.

(Можно задаться вопросом: если для правильного управления одним ресурсом требуется такой объем кода, что, если мой класс управляет более чем одним? Хотя это может показаться серьезной проблемой, и действительно, это требует нетривиальной попытки / catch , это не проблема. Это потому, что класс должен управлять только одним ресурсом !)

Успешное решение

Как уже упоминалось, идиома копирования и обмена исправит все эти проблемы.Но прямо сейчас у нас есть все требования, кроме одного: функция swap . Хотя Правило трех успешно влечет за собой существование нашего конструктора копирования, оператора присваивания и деструктора, его действительно следует называть «Большой тройкой с половиной»:Каждый раз, когда ваш класс управляет ресурсом, также имеет смысл предоставить функцию swap .

Нам нужно добавить функцию обмена в наш класс, и мы делаем это следующим образом †:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( Здесь объясняется, почему общедоступный обмен друзьями .) Теперь не только можем ли мы поменять местами наши dumb_array , но в целом свопы могут быть более эффективными; он просто меняет местами указатели и размеры, а не выделяет и копирует целые массивы. Помимо этого бонуса в функциональности и эффективности, теперь мы готовы реализовать идиому копирования и обмена.

Без лишних слов, наш оператор присваивания:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

Вот и все! Одним махом можно элегантно решить сразу все три проблемы.

Почему это работает?

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

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

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

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

Обратите внимание, что при входе в функцию все новые данные уже размещены, скопированы и готовы к использованию. Это то, что дает нам надежную бесплатную гарантию исключения: мы даже не войдем в функцию, если построение копии завершится неудачно, и, следовательно, невозможно изменить состояние * this . (То, что мы делали вручную для надежной гарантии исключений, теперь делает за нас компилятор; как любезно.)

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

Поскольку идиома не повторяет код, мы не можем вносить ошибки в оператор. Обратите внимание, что это означает, что мы избавляемся от необходимости проверки самоприсваивания, позволяя единую унифицированную реализацию operator = . (Кроме того, у нас больше нет потери производительности при назначении, не являющемся самим собой.)

И это идиома копирования и обмена.

А как насчет C ++ 11?

Следующая версия C ++, C ++ 11, вносит одно очень важное изменение в способ управления ресурсами: теперь «Правило трех» Правило четырех ] (и половина). Почему? Поскольку нам нужно не только иметь возможность копировать-конструировать наш ресурс, нам нужно также перемещать-конструировать .

К счастью для нас, это легко:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other)
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

Что здесь происходит? Вспомните цель move-Construction: взять ресурсы из другого экземпляра класса, оставив его в состоянии, которое гарантированно может быть присвоено и разрушаемо.

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

(Обратите внимание, что некоторые компиляторы не поддерживают делегирование конструктора; в этом случае мы должны вручную создать класс по умолчанию. Это неудачная, но, к счастью, тривиальная задача.)

Почему это работает?

То есть единственное изменение, которое нам нужно внести в наш класс, так почему оно работает? Вспомните важное решение, которое мы приняли, чтобы сделать параметр значением, а не ссылкой:

dumb_array& operator=(dumb_array other); // (1)

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

На этом заканчивается идиома копирования и обмена.


Сноски

* Почему мы устанавливаем mArray равным нулю? Потому что, если какой-либо следующий код в операторе выдает ошибку, может быть вызван деструктор dumb_array ; и если это произойдет, не установив для него значение null, мы попытаемся удалить уже удаленную память! Мы избегаем этого, установив для него значение null, поскольку удаление null не является операцией.

† Есть и другие утверждения о том, что мы должны специализировать std :: swap для нашего типа, предоставить внутриклассовый swap наряду со свободной функцией swap и т. Д. Но это все лишнее:любое правильное использование свопа будет происходить через неквалифицированный вызов, а наша функция будет найдена через ADL . Подойдет одна функция.

‡ Причина проста: если у вас есть ресурс, вы можете поменять его местами и / или переместить (C ++ 11) куда угодно. А сделав копию в списке параметров, вы максимизируете оптимизацию.

2087
ответ дан 22 November 2019 в 20:02
поделиться

Присвоение, по своей сути, состоит из двух шагов: снос старого состояния объекта и создание его нового состояния как копии состояния какого-то другого объекта.

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

В своей уточненной форме копирование и замена реализуется тем, что копирование выполняется путем инициализации (нессылочного) параметра оператора присваивания:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
262
ответ дан 22 November 2019 в 20:02
поделиться
Другие вопросы по тегам:

Похожие вопросы: