Почему некоторые люди используют своп для заданий на перемещение? - PullRequest
54 голосов
/ 14 июля 2011

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

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 ... в этом случае своп выполняет ненужную работу. Что мне не хватает? (unique_lock :: swap просто выполняет std :: swap для каждого члена)

Ответы [ 4 ]

87 голосов
/ 14 июля 2011

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

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

Нет причин использовать своп в этом примере.Это менее эффективно, чем вы предлагаете.Действительно, в libc ++ я делаю именно то, что вы предлагаете:

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

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

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

Примерно так:

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

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

Наблюдения:

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

mutex_type* __m_;
bool __owns_;

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

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

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

// 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% быстрее!

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

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

8 голосов
/ 14 июля 2011

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

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

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

  • Копирование-назначение с помощью copy-construct и swap.
  • Move-назначить с помощью move-construct и swap.
  • Напишите + в терминах конструкции и += и т. Д.

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

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

2 голосов
/ 24 ноября 2015

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

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

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

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

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

Вот пример:

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

Теперь давайте сравним две реализации перемещения-назначения:

// #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. Смотрите также обсуждение здесь в комментариях

2 голосов
/ 04 марта 2013

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

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

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

сгенерированный код в Move 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.Нет причин, по которым конструктор перемещения должен связываться с атомным рефконтером.

...