Move-конструктор через ручное копирование полей это ловушка. Скопировал указатель, забыл занулить источник, и оба объекта в деструкторе зовут delete по одному адресу. Double free: иногда падает сразу, иногда через неделю в проде.
std::exchange закрывает вопрос одним вызовом. Он забирает ресурс и кладёт в источник безопасное значение:
Handle(Handle&& other)
: ptr(std::exchange(other.ptr, nullptr)) {}
Новый объект получает старый указатель, а в other.ptr уезжает nullptr. delete по nullptr это no-op, поэтому перемещённый объект уничтожается безопасно, и владелец у ресурса ровно один.
Именно так пишет стандартная библиотека внутри. Коротко, exception-safe, без шанса забыть про зануление. Одна функция против двух строк, на которых легко ошибиться.
Правило короткое: перемещаешь владеющий указатель, бери std::exchange.
