Является ли эта реализация подкачки без уничтожения действительной в соответствии со стандартом? - PullRequest
0 голосов
/ 06 ноября 2018

Я предлагаю эту реализацию swap, если она действительна, превосходит текущую реализацию std::swap:

#include <new>
#include <type_traits>

template<typename T>
auto swap(T &t1, T &t2) ->
    typename std::enable_if<
        std::is_nothrow_move_constructible<T>::value
    >::type
{
    alignas(T) char space[sizeof(T)];
    auto asT = new(space) T{std::move(t1)};
        // a bunch of chars are allowed to alias T
    new(&t1) T{std::move(t2)};
    new(&t2) T{std::move(*asT)};
}

Страница для std::swap в cppreference подразумевает, что она использует перемещение-назначение, поскольку спецификация noexcept зависит от того, является ли перемещение-перемещение без-броска. Кроме того, здесь был задан вопрос , как swap реализован , и это то, что я вижу в реализациях для libstdc ++ и libc ++

template<typename T>
void typicalImplementation(T &t1, T &t2)
    noexcept(
        std::is_nothrow_move_constructible<T>::value &&
        std::is_nothrow_move_assignable<T>::value
    )
{
    T tmp{std::move(t1)};
    t1 = std::move(t2);
        // this move-assignment may do work on t2 which is
        // unnecessary since t2 will be reused immediately
    t2 = std::move(tmp);
        // this move-assignment may do work on tmp which is
        // unnecessary since tmp will be immediately destroyed
    // implicitly tmp is destroyed
}

Я не люблю использовать перемещение-присваивание, как в t1 = std::move(t2), потому что это подразумевает выполнение кода для освобождения ресурсов, хранящихся в t1, если ресурсы удерживаются, даже если известно, что ресурсы в t1 уже освобождены. У меня есть практический случай, когда освобождение ресурсов происходит через вызов виртуального метода, таким образом, компилятор не может устранить эту ненужную работу, потому что он не может знать виртуальный код переопределения, каким бы он ни был, ничего не будет делать, потому что нет ресурсы для релиза в t1.

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

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

  1. Эфемерный объект, созданный в tmp, не разрушается, но в пользовательском коде может быть некоторое предположение, что, если T создан, он будет разрушен
  2. T может быть типом с константами или ссылками, которые нельзя изменить, может быть реализовано назначение перемещения, меняя ресурсы, не касаясь этих констант или связывая ссылки.

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

Для иллюстрации я поместил ссылку на страницу проводника компилятора *1041*, которая показывает все три реализации для случая замены векторов целых чисел, то есть типичной реализации по умолчанию std::swap, специализированной для vector, и тот, который я предлагаю. Вы можете увидеть, что предлагаемая реализация выполняет меньше работы, чем обычная, точно так же, как специализированная в стандарте.

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

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

Ответы [ 2 ]

0 голосов
/ 06 ноября 2018

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

например. рассмотреть следующие вопросы:

class X
{
public:
    X(): resource_( std::move( allocate_resource() ) )

    X( X&& other ): X()
    {
        std::swap( resource_, other.resource_ );
    }

private:
    std::shared_ptr<Y> resource_;
};

тогда

X a;
X b;
swap( a, b );

Теперь, если своп реализован так, как вы предлагаете, в точке, где вы делаете

new(&t2) T{std::move(*asT)};

мы пропускаем экземпляр ресурса, так как конструктор перемещения выделяет один в * asT для замены того, который он украл, и это никогда не уничтожается.

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

0 голосов
/ 06 ноября 2018

Это незаконно, если T имеет ref или const членов. Используйте std::launder или сохраните новый указатель (см. [basic.life] p8 )

auto asT = new(space) T{std::move(t1)};
// or use std::launder

Но вам также нужно использовать std::launder для t1 и t2! И здесь у вас есть проблема, потому что без std::launder, t1 и t2 ссылаются на старое (уже уничтоженное) значение и не ссылаются на вновь созданный объект. Любой доступ к t1 и t2 будет тогда UB.

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

Преждевременная оптимизация? Теперь вам нужно вызвать два деструктора (t1 и t2)! Кроме того, вызов деструктора действительно не дорогой.

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

...