Семантика перемещения и Rvalues
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 83 из 104.
В C++ каждое выражение является либо lvalue (имеет постоянную идентичность, к нему можно обратиться по адресу), либо rvalue (временное, скоро будет уничтожено). Понимание этого различия открывает доступ к семантике перемещения — мощной оптимизации, которая позволяет избежать ненужного копирования.
rvalue-ссылка, объявляемая с помощью &&, привязывается именно к временным объектам. Это позволяет вам «красть» ресурсы у объектов, которые всё равно скоро исчезнут:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// Конструктор перемещения — забирает ресурсы
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Оставляем исходный объект в валидном состоянии
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // Вызывает конструктор перемещения
}Функция std::move на самом деле ничего не перемещает — она просто приводит lvalue к rvalue-ссылке, сигнализируя о том, что вы готовы отдать ресурсы объекта. Само перемещение происходит в конструкторе перемещения или операторе присваивания перемещением.
Семантика перемещения значительно повышает производительность при работе с ресурсоемкими объектами, такими как контейнеры или строки. Вместо глубокого копирования мегабайтов данных вы просто передаете владение указателем — это операция с константным временем выполнения, независимо от размера.
Задание
ЛегкоДавайте создадим класс DataBuffer для управления ресурсами, который демонстрирует семантику перемещения в действии. Вы увидите, как перемещение ресурсов вместо их копирования может значительно повысить эффективность при передаче владения динамически выделенной памятью.
Вы организуете свой код в трех файлах:
DataBuffer.h: Определите классDataBuffer, который управляет динамически выделенным массивом целых чисел.Ваш класс должен иметь приватные члены для указателя на данные (
int*), размера (size_t) и имени (std::string), чтобы помочь отслеживать, какой буфер задействован во время операций.Объявите следующее:
- Конструктор, который принимает имя
std::stringи размерsize_t, выделяет массив и выводит:[name] constructed with size [size] - Конструктор перемещения, который принимает rvalue-ссылку, забирает ресурсы и выводит:
[name] moved from [source_name](где перемещенный буфер принимает имя источника) - Деструктор, который выводит
[name] destroyed(илиempty destroyed, если ресурсы буфера были перемещены) - Метод
getSize(), который возвращает текущий размер - Метод
getName(), который возвращает имя буфера
Не забудьте пометить конструктор перемещения как
noexceptи оставить исходный объект в валидном пустом состоянии (nullptr, размер 0, имя "empty").- Конструктор, который принимает имя
DataBuffer.cpp: Реализуйте все методы, объявленные в заголовке. Когда запускается деструктор, удаляйте данные только в том случае, если указатель не равен null. Включите<iostream>для вывода.main.cpp: Считайте два входных значения:- Имя для вашего буфера (строка)
- Размер для вашего буфера (целое число)
Создайте
DataBufferс заданным именем и размером. Затем создайте второй буфер, переместив данные из первого с помощьюstd::move(). После перемещения выведите состояние обоих буферов:Original: [name] size=[size]New: [name] size=[size]
Включите
<utility>для использованияstd::move.
Например, при входных данных Alpha и 100:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyedПри входных данных Buffer и 50:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyedОбратите внимание, как конструктор перемещения передает владение выделенной памятью без копирования данных. Исходный буфер остается в пустом, но валидном состоянии, и когда оба буфера уничтожаются в конце программы, только тот, который все еще владеет памятью, фактически удаляет ее.
Шпаргалка
Выражения в C++ являются либо lvalue (постоянными, адресуемыми), либо rvalue (временными). Семантика перемещения оптимизирует производительность, передавая ресурсы от временных объектов вместо их копирования.
Rvalue-ссылки
Объявляемые с помощью &&, rvalue-ссылки привязываются к временным объектам:
Buffer(Buffer&& other) // Параметр rvalue-ссылкиКонструктор перемещения
«Крадет» ресурсы у временного объекта. Помечайте его как noexcept и оставляйте исходный объект в валидном пустом состоянии:
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Оставляем исходный объект валидным
other.size = 0;
}std::move
Приводит lvalue к rvalue-ссылке, сигнализируя о готовности передать ресурсы. Подключите заголовочный файл <utility>:
Buffer b1(1000);
Buffer b2(std::move(b1)); // Вызывает конструктор перемещенияstd::move сам по себе ничего не перемещает — фактическая передача происходит в конструкторе перемещения или операторе присваивания перемещением.
Преимущества
Семантика перемещения обеспечивает передачу ресурсов за константное время независимо от размера объекта, позволяя избежать дорогостоящего глубокого копирования больших структур данных.
Попробуйте сами
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: Создать DataBuffer с заданным name и size
// TODO: Создать второй буфер путем перемещения из первого с помощью std::move()
// TODO: Вывести состояние обоих буферов:
// Original: [name] size=[size]
// New: [name] size=[size]
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер12Возможности современного C++
Семантика перемещения и RvaluesИдеальная передача (Perfect Forwarding)Лямбда-выражения в деталяхstd::function и std::bindconstexpr и constevalСтруктурированные привязкиoptional, variant, any