Семантика перемещения в C++: std::move, rvalue‑ссылки и правило пяти — практическое руководство
Запрос, который часто вводят в поиск: «семантика перемещения c++ std::move примеры». Это руководство закрывает его полностью. Ниже — простые объяснения, много кода и практические советы, чтобы вы уверенно использовали перемещение в реальных проектах.
Что такое семантика перемещения в C++
Идея проста: вместо дорогого копирования ресурсов (памяти, файловых дескрипторов и т.д.) мы «передаём» владение ими другому объекту. Основа механики — rvalue‑ссылки (T&&) и std::move. В отличие от копии, перемещение делает старый объект «пустым», а новый — владельцем ресурса.
Практический пример: класс с ресурсом + логи
Сделаем небольшой класс Buffer, который владеет динамическим массивом. Добавим копирующие и перемещающие операции, а также логи, чтобы видеть, что именно вызвалось.
#include <iostream>
#include <vector>
#include <algorithm>
#include <utility>
class Buffer {
std::size_t size_ = 0;
int* data_ = nullptr;
public:
Buffer() = default;
explicit Buffer(std::size_t n) : size_(n), data_(n ? new int[n]{} : nullptr) {
std::cout << "Ctor: size=" << size_ << 'n';
}
~Buffer() {
delete[] data_;
std::cout << "Dtor: size=" << size_ << 'n';
}
Buffer(const Buffer& other) : size_(other.size_), data_(other.size_ ? new int[other.size_] : nullptr) {
std::cout << "Copy ctorn";
if (data_) std::copy(other.data_, other.data_ + size_, data_);
}
Buffer& operator=(const Buffer& other) {
std::cout << "Copy assignn";
if (this == &other) return *this;
Buffer tmp(other); // копия
swap(tmp);
return *this;
}
Buffer(Buffer&& other) noexcept : size_(std::exchange(other.size_, 0)),
data_(std::exchange(other.data_, nullptr)) {
std::cout << "Move ctorn";
}
Buffer& operator=(Buffer&& other) noexcept {
std::cout << "Move assignn";
if (this == &other) return *this;
delete[] data_;
size_ = std::exchange(other.size_, 0);
data_ = std::exchange(other.data_, nullptr);
return *this;
}
void swap(Buffer& other) noexcept {
std::swap(size_, other.size_);
std::swap(data_, other.data_);
}
std::size_t size() const noexcept { return size_; }
};
int main() {
Buffer a(1'000'000); // большой буфер
std::vector<Buffer> v;
v.reserve(3);
v.push_back(a); // КОПИЯ
v.push_back(std::move(a)); // ПЕРЕМЕЩЕНИЕ, a становится пустым
v.emplace_back(500'000); // Конструирование на месте, без лишних копий
std::cout << "a.size=" << a.size() << 'n'; // обычно 0 после перемещения
}
Скомпилируйте и посмотрите вывод. Вы увидите «Copy ctor/assign» и «Move ctor/assign» в разных местах. Это лучший способ прочувствовать механику на практике.
Когда и как правильно использовать std::move
Типичные ошибки и как их исправить
1) Использование объекта после перемещения
Buffer b(10);
Buffer c = std::move(b);
// b в валидном, но пустом состоянии. Полагаться на старое содержимое нельзя.
std::cout << b.size() << 'n'; // допустимо, но здесь скорее всего 0
Совет: сразу переиспользуйте объект в новом качестве (переинициализируйте) или не трогайте его вовсе.
2) std::move от const-объекта — это копия, а не перемещение
const Buffer c(42);
std::vector<Buffer> v;
v.push_back(std::move(c)); // вызовется КОПИЯ, т.к. move-ctor: Buffer(Buffer&&), а не const Buffer&&
Совет: перемещать имеет смысл из неконстантных объектов. Если объект должен быть перемещаемым, не делайте его const в той точке, где планируете перемещать.
3) Нет noexcept у move — контейнеры будут копировать
Стандартные контейнеры при перераспределении памяти предпочитают перемещать элементы, но только если move-конструктор помечен noexcept. Иначе для безопасности они используют копию.
struct X {
X() = default;
X(X&&) noexcept { /* ... */ } // Добавьте noexcept!
};
4) std::move не двигает сам по себе
Это всего лишь «каст» к rvalue. Двигать будет ваш move‑конструктор/оператор, если они определены и доступны. Без них — будет копия или ошибка компиляции.
Коротко о std::forward и perfect forwarding
Если вы пишете шаблонные обёртки/фабрики и хотите «пробрасывать» аргументы без лишних копий, используйте forwarding-ссылки и std::forward.
#include <utility>
template <class F, class... Args>
auto call(F&& f, Args&&... args) {
return std::forward<F>(f)(std::forward<Args>(args)...);
}
Здесь lvalue-аргументы останутся lvalue, rvalue — rvalue. Это не про ускорение само по себе, а про сохранение «категории значения», чтобы в глубине стека вызовов сработало именно перемещение там, где это возможно.
Лучшие практики: правило пяти и не только
Немного про взаимодействие с контейнерами
Чек‑лист перед ревью кода
Хотите системно закрыть пробелы в базовых и продвинутых темах C++ с практикой и домашними заданиями? Посмотрите программу и первые уроки в курсе «Программирование на C++ с Нуля до Гуру» — начать обучение и прокачать C++ сейчас.
Итоги
Семантика перемещения — один из самых ощутимых «бонусов» современного C++. Освойте rvalue‑ссылки, std::move и правило пяти, помечайте move‑операции noexcept, избегайте распространённых ловушек — и вы получите быстрый, аккуратный и предсказуемый код. Используйте приведённый пример как шаблон и смело внедряйте перемещение в свои проекты.





