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


Что это за идиома и когда ее следует использовать? Какие проблемы она решает? Меняется ли идиома при использовании C++11?

хотя это было упомянуто во многих местах, у нас не было никаких особых вопросов и ответов "что это такое", так что вот оно. Вот неполный список мест, где это было ранее упомянуто:

5 1709

5 ответов:

обзор

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

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

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

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

принципиально, он работает с помощью функции copy-constructor для создания локальной копии данных, а затем принимает скопированные данные с swap функция, обменивая старые данные с новые данные. Временная копия затем разрушается, забирая старые данные с собой. Мы остаемся с копией новых данных.

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

функция подкачки-это не бросать функция, которая меняет местами два объекта класса, член за членом. У нас может возникнуть соблазн использовать std::swap вместо того, чтобы обеспечить наше собственное, но это было бы невозможно; std::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. во-вторых, он обеспечивает только базовую гарантию исключения. Если new int[mSize] не, *this будут изменены. (А именно, размер неверен, и данные исчезли!) Для сильной гарантии исключения, это должно быть что-то сродни:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
    
  3. код расширился! Что приводит нас к третьей проблеме: дублирование кода. Наш оператор присваивания фактически дублирует весь код, который мы уже написали в другом месте, и это ужасно.

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

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

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

как уже упоминалось, идиома копирования и замены исправит все эти проблемы. Но сейчас у нас есть все требования, кроме одного:. А Правило трех успешно влечет за собой существование нашего конструктора копирования, оператора присваивания и деструктора, его действительно следует называть "Большой тройкой с половиной": каждый раз, когда ваш класс управляет ресурсом, также имеет смысл предоставить .

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

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);
    }

    // ...
};

(здесь - это объяснение, почему public friend swap.) Теперь мы не только можем поменять наши dumb_array'S, но свопы в general может быть более эффективным; он просто меняет указатели и размеры, а не выделяет и копирует целые массивы. Помимо этого бонуса в функциональности и эффективности, теперь мы готовы реализовать идиому копирования и замены.

без дальнейших церемоний, наш оператор присваивания:

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), а затем поменять местами с other; мы знаем, что построенный по умолчанию экземпляр нашего класса может быть безопасно назначен и уничтожен, поэтому мы знаем other сможете сделать то же самое, после замены.

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

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

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

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

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

и так завершается идиома копирования и замены.


сноски

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

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

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

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

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

в своей уточненной форме copy-and-swap реализуется путем выполнения копирования путем инициализации (без ссылки) параметра оператора присваивания:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}

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

что такое идиома копирования и замены?

способ реализации оператора присваивания в терминах функции подкачки:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

основная идея заключается в том, что:

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

  • это приобретение может быть предпринято до изменение текущего состояния объекта (т. е. *this) если сделана копия нового значения, то именно поэтому rhs принят по стоимости (т. е. копируются), а не по ссылке

  • замена состояния локальной копии rhs и *this - это обычно относительно легко обойтись без потенциальных сбоев / исключений, учитывая, что локальная копия не нуждается в каком-либо конкретном состоянии после этого (просто нужно состояние, пригодное для запуска деструктора, так же как и для объекта двигался from in > = C++11)

когда он должен быть использован? (Какие проблемы он решает [/create]?)

  • когда вы хотите назначено возражал не зависит от назначения, которое вызывает исключение, предполагая, что у вас есть или может написать swap С сильной гарантией исключения, и в идеале тот, который не может потерпеть неудачу/throw..†

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

    • самоназначение, выполненное как копирование и замена, позволяет избежать часто упускаемого края случаи.‡

  • когда любое снижение производительности или мгновенное увеличение использования ресурсов, вызванное наличием дополнительного временного объекта во время назначения, не имеет значения для вашего приложения. ⁂

swap throwing: как правило, можно надежно поменять местами элементы данных, которые объекты отслеживают указателем, но не указатели элементы данных, которые не имеют throw-free swap, или для которых swapping должен быть реализован как X tmp = lhs; lhs = rhs; rhs = tmp; и копирование-строительство или назначение может бросить, по-прежнему есть потенциал, чтобы не оставить некоторые члены данных поменялись местами, а другие нет. Этот потенциал применим даже к C++03 std::stringкак Джеймс комментирует другой ответ:

@wilhelmtell: в C++03 нет упоминания об исключениях, потенциально вызванных std:: string:: swap (который вызывается std:: swap). В C++0x std::string::swap является noexcept и не должен вызывать исключения. – Джеймс McNellis 22 декабря '10 в 15:24


implementation реализация оператора присваивания, которая кажется разумной при назначении из отдельного объекта, может легко потерпеть неудачу для самостоятельного назначения. Хотя может показаться невообразимым, что клиентский код даже попытается самоназначение, это может произойти относительно легко во время операций algo с контейнерами, с x = f(x); код f это (возможно, только для некоторых #ifdef ветки) макрос Ала #define f(x) x или функция, возвращающая ссылку на x, или даже (скорее всего неэффективный, но лаконичный) код типа x = c1 ? x * 2 : c2 ? x / 2 : x;). Например:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

при самостоятельном назначении, приведенный выше код удаления x.p_;, пункты p_ в недавно выделенной области кучи, затем пытается прочитать неинициализированное данные в нем (неопределенное поведение), если это не делает ничего слишком странного,copy попытки самоназначения для каждого только что разрушенного "Т"!


idiom идиома копирования и замены может привести к неэффективности или ограничениям из-за к использованию дополнительного временного (когда параметр оператора копируется):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

вот, рукописный Client::operator= может проверить, если *this уже подключен к тому же серверу,rhs (возможно, отправка кода "сброса", если это полезно), тогда как подход копирования и подкачки вызовет конструктор копирования, который, вероятно, будет написан, чтобы открыть отдельное соединение сокета, а затем закрыть исходное. Это не только может означать удаленное сетевое взаимодействие вместо простая копия переменной в процессе, она может столкнуться с ограничениями клиента или сервера на ресурсы сокета или соединения. (Конечно, этот класс имеет довольно ужасный интерфейс, но это другое дело ;-P).

этот ответ больше похож на дополнение и небольшое изменение в вышеуказанные ответы.

в некоторых версиях Visual Studio (и, возможно, других компиляторов) есть ошибка, которая действительно раздражает и не имеет смысла. Поэтому, если вы объявите / определите свой

я хотел бы добавить слово предупреждения, когда вы имеете дело с контейнерами с распределителем в стиле C++11. Обмен и назначение имеют тонко различную семантику.

для конкретности рассмотрим контейнер std::vector<T, A>, где A - это некоторый тип распределителя с сохранением состояния, и мы сравним следующие функции:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

назначение обеих функций fs и fm дать a государство b изначально. Тем не менее, есть скрытый вопрос: Что произойдет, если a.get_allocator() != b.get_allocator()? Ответ таков: это зависит. Давайте напишем AT = std::allocator_traits<A>.

  • если AT::propagate_on_container_move_assignment и std::true_type, потом fm переназначает распределителя a со значением b.get_allocator(), иначе не получится, а a продолжает использовать свой аллокатор. В этом случае элементы данных должны быть заменены индивидуально, так как хранение a и b не совместимы.

  • если AT::propagate_on_container_swap is std::true_type, потом fs обменивает данные и распределители ожидаемым образом.

  • если AT::propagate_on_container_swap и std::false_type, тогда нам нужна динамическая проверка.

    • если a.get_allocator() == b.get_allocator(), затем два контейнера используют совместимое хранилище, и обмен происходит обычным образом.
    • однако, если a.get_allocator() != b.get_allocator() программа неопределено поведение (ср. МФ. [контейнер.требования.general/8].

в в результате обмен стал нетривиальной операцией в C++11, как только ваш контейнер начинает поддерживать распределители с отслеживанием состояния. Это несколько "продвинутый вариант использования", но это не совсем маловероятно, поскольку оптимизация перемещения обычно становится интересной только после того, как ваш класс управляет ресурсом, а память является одним из самых популярных ресурсов.