Menu
Coddy logo textTech

Семантика перемещения и 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-ссылке, сигнализируя о том, что вы готовы отдать ресурсы объекта. Само перемещение происходит в конструкторе перемещения или операторе присваивания перемещением.

Семантика перемещения значительно повышает производительность при работе с ресурсоемкими объектами, такими как контейнеры или строки. Вместо глубокого копирования мегабайтов данных вы просто передаете владение указателем — это операция с константным временем выполнения, независимо от размера.

challenge icon

Задание

Легко

Давайте создадим класс 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: Считайте два входных значения:
    1. Имя для вашего буфера (строка)
    2. Размер для вашего буфера (целое число)

    Создайте 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;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование