Подтвердить что ты не робот

Почему некоторые люди используют swap для переадресации?

Например, stdlibС++ имеет следующее:

unique_lock& operator=(unique_lock&& __u)
{
    if(_M_owns)
        unlock();
    unique_lock(std::move(__u)).swap(*this);
    __u._M_device = 0;
    __u._M_owns = false;
    return *this;
}

Почему бы просто не назначить двух членов __u * непосредственно этому? Не имеет ли своп подразумевать, что __u назначается * этим членам, только для того, чтобы позже назначить 0 и false... в этом случае своп делает ненужную работу. Что мне не хватает? (уникальный_lock:: swap просто выполняет std:: swap для каждого члена)

4b9b3361

Ответ 1

Это моя вина. (наполовину шучу, полу-не).

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

В этом примере нет причин использовать swap. Он менее эффективен, чем вы предлагаете. Действительно, в libС++ я делаю именно то, что вы предлагаете:

unique_lock& operator=(unique_lock&& __u)
    {
        if (__owns_)
            __m_->unlock();
        __m_ = __u.__m_;
        __owns_ = __u.__owns_;
        __u.__m_ = nullptr;
        __u.__owns_ = false;
        return *this;
    }

В общем случае оператор присваивания перемещения должен:

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

Так же:

unique_lock& operator=(unique_lock&& __u)
    {
        // 1. Destroy visible resources
        if (__owns_)
            __m_->unlock();
        // 2. Move assign all bases and members.
        __m_ = __u.__m_;
        __owns_ = __u.__owns_;
        // 3. If the move assignment of bases and members didn't,
        //           make the rhs resource-less, then make it so.
        __u.__m_ = nullptr;
        __u.__owns_ = false;
        return *this;
    }

Обновление

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

Вопрос: какой лучший шаблон для создания конструктора перемещения? Делегировать конструктор по умолчанию и затем поменять местами? Преимущество этого заключается в уменьшении дублирования кода.

Мой ответ: я думаю, что самый важный взлет - это то, что программисты должны избегать следующих шаблонов без размышлений. Могут быть некоторые классы, где реализация конструктора перемещения по умолчанию + swap - это точно правильный ответ. Класс может быть большим и сложным. A(A&&) = default; может ошибиться. Я думаю, что важно рассмотреть все ваши варианты для каждого класса.

Подробно рассмотрим пример OP: std::unique_lock(unique_lock&&).

замечания:

а. Этот класс довольно прост. Он имеет два элемента данных:

mutex_type* __m_;
bool __owns_;

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

С. Конструктор перемещения для этого класса будет состоять из небольшого количества загрузок и хранилищ, несмотря ни на что. Таким образом, хороший способ взглянуть на производительность - подсчитать нагрузки и магазины. Например, если вы делаете что-то с 4 магазинами, а кто-то другой делает то же самое только с двумя магазинами, обе ваши реализации очень быстры. Но их дважды так же быстро, как и ваши! Это различие может иметь решающее значение в некоторых узких циклах клиента.

Сначала давайте посчитаем нагрузки и хранилища в конструкторе по умолчанию и в функции замены членов:

// 2 stores
unique_lock()
    : __m_(nullptr),
      __owns_(false)
{
}

// 4 stores, 4 loads
void swap(unique_lock& __u)
{
    std::swap(__m_, __u.__m_);
    std::swap(__owns_, __u.__owns_);
}

Теперь реализуем конструктор перемещения двумя способами:

// 4 stores, 2 loads
unique_lock(unique_lock&& __u)
    : __m_(__u.__m_),
      __owns_(__u.__owns_)
{
    __u.__m_ = nullptr;
    __u.__owns_ = false;
}

// 6 stores, 4 loads
unique_lock(unique_lock&& __u)
    : unique_lock()
{
    swap(__u);
}

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

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

Первый способ быстрее. Если стоимость загрузок и магазинов примерно одинакова, то, возможно, на 66% быстрее!

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

Для libС++ я выбрал более быстрое решение. Мое логическое обоснование заключается в том, что для этого класса я лучше понимаю его; класс достаточно прост, что мои шансы получить его правильно высоки; и мои клиенты будут оценивать производительность. Я мог бы прийти к другому выводу для другого класса в другом контексте.

Ответ 2

О безопасности исключений. Поскольку __u уже сконструирован при вызове оператора, мы не знаем об исключении, а swap не бросаем.

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

Возможно, в этом тривиальном примере это не отображается, но это общий принцип проектирования:

  • Скопировать-присваивать с помощью copy-construct и swap.
  • Переместить-присваивать с помощью move-construct и swap.
  • Напишите + в терминах конструкции и += и т.д.

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

(unique_ptr принимает явное задание rvalue в присваивании, потому что оно не позволяет создавать/назначать копию, поэтому это не лучший пример этого принципа проектирования.)

Ответ 3

Еще одна вещь, которую следует учитывать в отношении компромисса:

Реализация по умолчанию - конструкция + своп может появляться медленнее, но иногда - анализ потока данных в компиляторе может устранить некоторые бессмысленные назначения и в итоге очень похож на рукописный код. Это работает только для типов без "умной" семантики значений. В качестве примера,

 struct Dummy
 {
     Dummy(): x(0), y(0) {} // suppose we require default 0 on these
     Dummy(Dummy&& other): x(0), y(0)
     {
         swap(other);             
     }

     void swap(Dummy& other)
     {
         std::swap(x, other.x);
         std::swap(y, other.y);
         text.swap(other.text);
     }

     int x, y;
     std::string text;
 }

сгенерированный код в движении ctor без оптимизации:

 <inline std::string() default ctor>
 x = 0;
 y = 0;
 temp = x;
 x = other.x;
 other.x = temp;
 temp = y;
 y = other.y;
 other.y = temp;
 <inline impl of text.swap(other.text)>

Это выглядит ужасно, но анализ потока данных может определить, что он эквивалентен коду:

 x = other.x;
 other.x = 0;
 y = other.y;
 other.y = 0;
 <overwrite this->text with other.text, set other.text to default>

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

Есть также случаи, когда обмен происходит лучше, чем назначение из-за "умной" семантики значения, например, если один из членов класса является std:: shared_ptr. Нет причин, по которым конструктор перемещения должен взаимодействовать с атомарным refcounter.

Ответ 4

Я отвечу на вопрос из заголовка: "Почему некоторые люди используют swap для назначения переноса?".

Основная причина использования swap - , обеспечивающая неперехватывание перемещения.

Из комментария Ховарда Хиннанта:

В общем случае оператор присваивания перемещения должен:
1. Уничтожьте видимые ресурсы (хотя, возможно, сохраните ресурсы детализации реализации).

Но вообще функция destroy/release может выйти из строя и исключить исключение!

Вот пример:

class unix_fd
{
    int fd;
public:
    explicit unix_fd(int f = -1) : fd(f) {}
    ~unix_fd()
    {
        if(fd == -1) return;
        if(::close(fd)) /* !!! call is failed! But we can't throw from destructor so just silently ignore....*/;
    }

    void close() // Our release-function
    {
        if(::close(fd)) throw system_error_with_errno_code;
    }
};

Теперь сравните две реализации присваивания move:

// #1
void unix_fd::operator=(unix_fd &&o) // Can't be noexcept
{
    if(&o != this)
    {
        close(); // !!! Can throw here
        fd = o.fd;
        o.fd = -1;
    }
    return *this;
}

и

// #2
void unix_fd::operator=(unix_fd &&o) noexcept
{
    std::swap(fd, o.fd);
    return *this;
}

#2 совершенно не исключено!

Да, вызов close() может быть "задержан" в случае #2. Но! Если нам нужна строгая проверка ошибок, мы должны использовать явный вызов close(), а не деструктор. Деструктор освобождает ресурс только в ситуациях "чрезвычайной ситуации", где вытеснение не может быть выброшено в любом случае.

P.S. См. Также обсуждение здесь в комментариях