Является ли эта реализация подкачки без разрушения допустимой в соответствии со стандартом?

Я предлагаю эту реализацию 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 выше.

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

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

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


person TheCppZoo    schedule 05.11.2018    source источник
comment
Назначение перемещения может быть быстрее, чем построение перемещения. И я почти уверен, что если вы поместите новый объект в память действительного объекта, то этот действительный объект никогда не будет уничтожен.   -  person xaxxon    schedule 06.11.2018
comment
Согласно той части стандарта, которую Rakete упоминает ниже, использование этой формы для любого типа, который зависит от его деструктора, является UB.   -  person xaxxon    schedule 06.11.2018
comment
std::swap также не заставляет пользователя думать, что будут вызываться конструкторы, поэтому такое поведение вводит в заблуждение. Скажем, у вас есть счетчик экземпляров объекта в вашем типе... каждый раз, когда вы вызываете swap, он увеличивается на 3? смысла в обмене нет   -  person xaxxon    schedule 06.11.2018
comment
@xaxxon это ссылка на то, как libstdc++ реализует своп, который использует конструкторы перемещения: github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/   -  person TheCppZoo    schedule 06.11.2018
comment
Он использует конструктор перемещения только тогда, когда это необходимо — когда он создает новый объект (__tmp) — после этого он использует присваивание перемещения.   -  person xaxxon    schedule 06.11.2018
comment
Строка auto lt1 = std::launder(&t1); делает lt1 пригодным для доступа к новому объекту, но не влияет на t1, поэтому использование t1 для доступа к новому объекту по-прежнему UB.   -  person alain    schedule 06.11.2018
comment
Конструктор перемещения также может выполнять ненужную работу с перемещаемым объектом, как и присваивание.   -  person xaxxon    schedule 06.11.2018


Ответы (2)


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

person Rakete1111    schedule 05.11.2018
comment
Им также нужно вызвать деструктор t1 перед вызовом new(&t1) T{std::move(t2)};, не так ли? (то же Q относится и к t2) - person NathanOliver; 06.11.2018
comment
@NathanOliver На самом деле нет, но вы правы, если вы этого не сделаете, это плохая идея;) - person Rakete1111; 06.11.2018
comment
законно ли время жизни объекта заканчиваться без вызова его деструктора? - person xaxxon; 06.11.2018
comment
@xaxxon jup, даже если это нетривиально eel.is/ c++draft/basic.life#5.sentence-2 - person Rakete1111; 06.11.2018
comment
хорошо... так что важная часть... любая программа, которая зависит от побочных эффектов, создаваемых деструктором, имеет неопределенное поведение. Кажется, вы должны четко указать это в своем ответе. - person xaxxon; 06.11.2018
comment
@ Rakete1111 Rakete1111 Мое чтение этого абзаца наводит меня на мысль, что вне свопа, когда объект уничтожается и вызывается деструктор, а затем это UB. Разве это не так? - person NathanOliver; 06.11.2018
comment
@xaxxon Насколько я понимаю, эта часть бесполезна. Вы не можете зависеть от чего-то, чего не говорит стандарт. Это похоже на предложение, в котором говорится, что любая программа, которая полагается на 1 + 1, вызывающую функцию f(), имеет UB. - person Rakete1111; 06.11.2018
comment
@NathanOliver Это тоже так. Проблема в том, что переменная t не относится к новому объекту. Они по-прежнему ссылаются на старое значение. Итак, когда вызывается неявно вызываемый деструктор, вы обращаетесь к объекту и получаете UB - person Rakete1111; 06.11.2018
comment
@ Rakete1111 Rakete1111 В четко определенной программе побочные эффекты от деструктора объекта должны возникать, когда время жизни объекта заканчивается, верно? Поэтому, если вы собираетесь пропустить вызов деструктора, вы должны априори знать, что побочных эффектов нет, что является плохим выбором в универсальном коде, таком как std::swap - person xaxxon; 06.11.2018
comment
@xaxxon нет, вот что говорит это предложение. Вам не нужно звонить. Да, в этом суть. Вы должны вызывать его в универсальном коде, потому что это может иметь побочные эффекты. - person Rakete1111; 06.11.2018
comment
@ Rakete1111 Rakete1111 Я включил в сводку три выдвинутых вами возражения: 1. отмыть, 2. const, ссылочные члены экземпляра, 3. побочные эффекты деструктора. Это делает законным для очень многих типов, если пользователи не запускают UB. - person TheCppZoo; 06.11.2018
comment
@TheCppZoo На самом деле проблемой являются только константные и ссылочные члены (и если они вложены, но это было бы странно). Но для универсальной функции это не годится. - person Rakete1111; 06.11.2018
comment
@ Rakete1111 признаков, исключающих эти два случая, не существует, но код, который я предлагаю, является допустимым выбором для пользователей, чтобы поменять местами таким образом, если они знают, что их типы не достигают 1 или 2. Кроме того, нет способа найти если все перестановки перемещения-конструкции более эффективны, чем одно построение перемещения и два назначения перемещения, это по-прежнему остается за пользователем. - person TheCppZoo; 06.11.2018
comment
@ Rakete1111 Rakete1111 кажется, вы сказали в комментарии, что в силу получения конструкции t1 и t2 недействительны из swap, однако eel.is/c++draft/basic.life#8.sentence-1 говорит, что они действительны, если вы принимаете время жизни исходных объектов, завершившееся при их получатели размещения-новый и 6.8.3 не применяется... - person TheCppZoo; 06.11.2018
comment
@TheCppZoo Они недействительны только в том случае, если у них есть члены const или ref (когда они не удовлетворяют p8), в противном случае они в порядке :) - person Rakete1111; 06.11.2018
comment
@ Rakete1111 недействительно, только если Почему вы вообще предлагаете std::launder? - person curiousguy; 07.11.2018
comment
@curiousguy, потому что для доступа к новым значениям через ts вам нужно отмыть - person Rakete1111; 07.11.2018
comment
@ Rakete1111 Нельзя ожидать, что программист поймет, что он должен использовать std::launder после вызова функции с именем swap, вы согласны? - person curiousguy; 07.11.2018
comment
@curiousguy Полностью согласен. :) - person Rakete1111; 07.11.2018
comment
@ Rakete1111 Таким образом, на практике ни один тип, для которого новое размещение не гарантирует сохранение действительности существующих указателей и ссылок, не должен использовать этот метод замены. Это означает, что вам не нужно использовать std::launder для t1 и t2! - person curiousguy; 07.11.2018
comment
@curiousguy ну да, но для общего обмена это не очень хорошая идея - person Rakete1111; 07.11.2018
comment
Давайте продолжим обсуждение в чате. - person curiousguy; 07.11.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 для замены того, который он украл, и он никогда не уничтожается.

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

person moonshadow    schedule 05.11.2018
comment
конструкторы перемещения и операторы присваивания должны оставлять свои аргументы в допустимом состоянии. Что вы подразумеваете под действительным? Они точно должны быть разрушаемыми, но есть ли на самом деле другие требования? - person xaxxon; 06.11.2018
comment
xaxxon stackoverflow.com/questions/12095048/ - person moonshadow; 06.11.2018
comment
@moonshadow Как видите, все операции в предложенном swap оставляют объекты в допустимом состоянии (происходит только перемещение конструкций допустимых объектов). Утечка ресурсов невозможна, если: 1. конструкторы перемещения не утекают и 2. нет ресурса, полученного при построении перемещения, который не передается в качестве источника построения перемещения (что потребовало бы освобождения в деструкторе). ), я думаю, что сценарий (магические ресурсы, полученные при строительстве, но не выпущенные, являющиеся источником перемещения-строительства) не будет слишком ограничивать пользователей моего кода - person TheCppZoo; 06.11.2018