JavaRush /Курсы /C++ SELF /`=delete` — запреты копирования как защита дизайна

`=delete` — запреты копирования как защита дизайна

C++ SELF
45 уровень , 1 лекция
Открыта

1. Зачем нужен =delete

Когда вы впервые видите =delete, мозг легко уезжает в сторону «ага, это как delete p; — сейчас будем освобождать память». И вот тут C++ радостно подставляет ножку: это разные “delete”, просто слово одинаковое. delete p; — это оператор освобождения динамической памяти (и мы договорились, что в прикладном коде стараемся туда не лезть). А =delete — это способ сказать компилятору: «Функция существует как часть интерфейса, но вызывать её нельзя».

По сути, =delete — это ваш способ встроить «красную кнопку» прямо в тип: если кто-то попытается сделать запрещённую вещь, программа даже не соберётся. И это прекрасно, потому что компилятор ловит ошибку раньше, чем пользователь ловит segmentation fault (или ваш тимлид ловит вас в коридоре).

Держите простую картинку: функция объявляется, участвует в выборе перегрузок, но помечена как «удалённая». Если код пытается её использовать, компилятор выдаёт ошибку вида «use of deleted function».

Эта идея важна по двум причинам. Во-первых, вы можете запрещать не только конструкторы/копирование, но и обычные функции и перегрузки. Во-вторых, удалённая функция иногда даёт более понятную диагностику, чем «ой, у вас тут что-то не подходит по типам, я выбрал странную перегрузку и теперь всё сломалось».

Кстати, стандартная библиотека тоже активно использует удалённые функции, чтобы запретить бессмысленные или опасные операции (например, у некоторых типов могут быть удалены конструкторы по умолчанию). В черновиках стандарта C++ такие места встречаются регулярно — например, можно наткнуться на случаи вроде atomic_ref() = delete; как явный запрет определённого сценария использования.

2. Запрещаем создание «пустого» объекта

Очень частая проблема новичка: мы создаём тип, а потом вдруг обнаруживаем, что «объект без данных» вообще не имеет смысла, но язык по умолчанию позволяет написать T t{};. В результате в программе появляются сущности в странном состоянии: вроде объект есть, а пользоваться им нельзя — как «аккаунт без логина», только хуже, потому что компилятор не в курсе вашего жизненного опыта.

Если объект обязан иметь значение, запретите конструктор по умолчанию.

#include <iostream>

struct TaskId {
    int value{};

    TaskId() = delete;                 // запрещаем "пустой" id
    explicit TaskId(int v) : value(v) {}
};

int main() {
    TaskId id{10};
    std::cout << id.value << '\n';     // 10
}

Обратите внимание на психологический эффект: тип сам рассказывает о правилах игры. Вам не нужно надеяться, что все прочитали комментарий «нельзя создавать без id». Комментарии читают редко. Компилятор — всегда.

Теперь встроим это в наше учебное приложение. Пусть у нас есть простая модель задачи (микро‑todo менеджер), которую мы развиваем ещё со времён struct, vector и функций.

#include <string>

struct Task {
    int id{};
    std::string title;
    bool done{false};
};

Задача с id=0 может быть валидной или нет — зависит от ваших правил. Но если вы решите, что id должен быть строго положительным и задаваться при создании, то Task() тоже можно запретить (или сделать конструктор, требующий id и title).

3. Запрещаем копирование: non-copyable типы

Самое вкусное начинается, когда вы запрещаете копирование. Здесь важно не путать: запрет копирования — это не «оптимизация», а в первую очередь смысл. Есть типы, которые по смыслу нельзя копировать, потому что копия разрушает модель мира.

Классические случаи выглядят так: объект владеет чем-то уникальным (файл, соединение, «единственный генератор id», «единственный активный сеанс»). Если такой объект скопировать, у вас внезапно окажутся «два единственных» владельца, и это звучит как плохая философия и ещё более плохая инженерия.

Как запретить копирование

Синтаксис запрета копирования обычно такой: удаляем копирующий конструктор и копирующее присваивание.

#include <iostream>

struct IdGenerator {
    int nextId{1};

    IdGenerator() = default;

    IdGenerator(const IdGenerator&) = delete;
    IdGenerator& operator=(const IdGenerator&) = delete;

    int allocate() {
        return nextId++;
    }
};

int main() {
    IdGenerator gen;
    std::cout << gen.allocate() << '\n'; // 1
    std::cout << gen.allocate() << '\n'; // 2
}

Почему это полезно для нашего приложения? Потому что генератор id — это отличный пример «уникальной сущности». Если его случайно скопировать, то две копии начнут выдавать одинаковые id, и ваша база задач превратится в фестиваль коллизий: «задача 7» будет встречаться чаще, чем слово «компилятор» на лекции по C++.

=delete и сигнатуры функций: по значению обычно нужна копия

Когда вы запрещаете копирование, всплывает важная практическая вещь: не все сигнатуры функций совместимы с non-copyable типами. И это не «ограничение языка», а прямое следствие смысла: если тип нельзя копировать, то функция, которая принимает его по значению, автоматически становится подозрительной.

Покажем на нашем IdGenerator. Если мы захотим написать функцию, которая «покажет текущий nextId», нам не нужно копировать генератор — достаточно ссылки.

#include <iostream>

struct IdGenerator {
    int nextId{1};
    IdGenerator(const IdGenerator&) = delete;
    IdGenerator& operator=(const IdGenerator&) = delete;
};

void printState(const IdGenerator& gen) {
    std::cout << gen.nextId << '\n';
}

int main() {
    IdGenerator gen;
    printState(gen); // 1
}

А вот так делать нельзя (и это правильно):

// void printState(IdGenerator gen); // потребовало бы копирование: ошибка

И тут компилятор становится вашим союзником: он не даст «случайно» спроектировать API так, будто тип копируемый. Вам придётся быть честным: либо const& для чтения, либо & для изменения.

Запрет копирования без «дыр»: удаляем обе операции

Есть очень распространённая поломка дизайна: запретили копирующий конструктор, но забыли запретить копирующее присваивание (или наоборот). Тогда тип получается «наполовину запрещённый»: создать копию нельзя, но можно присвоить значение в существующий объект — и это может быть либо бессмысленно, либо опасно, либо просто странно.

Давайте посмотрим на примере.

struct Weird {
    int x{};

    Weird(const Weird&) = delete;      // запретили копию при создании
    // Weird& operator=(const Weird&) = ???  // забыли
};

Теперь Weird b = a; запрещено, но b = a; может внезапно оказаться разрешено (если компилятор сгенерирует присваивание). Это выглядит как правило «копировать нельзя, но если очень хочется — то можно». Обычно так не задумывают.

Поэтому в учебной и реальной практике чаще всего действует простое правило: если тип по смыслу non-copyable, удаляем и копирующий конструктор, и копирующее присваивание. Тогда «дыр» не остаётся.

4. Управляем интерфейсом: удаляем нежелательные перегрузки

До этого момента мы использовали =delete как инструмент для спец‑функций (конструкторы/копирование). Но он так же полезен для «обычных» методов, когда вы хотите запретить конкретную форму вызова.

Ситуация из реального мира: вы пишете функцию setTitle, и вам важно, чтобы заголовок был именно std::string, а не сырой const char*. Почему? Например, потому что вы хотите явно отделять «строку как объект» от «указателя на память, где где-то лежат символы». Или потому что вы делаете перегрузки и не хотите случайных неявных преобразований.

Сделаем маленький компонент для нашего приложения — «редактор задачи».

#include <string>

struct TaskEditor {
    std::string title;

    void setTitle(const std::string& s) {
        title = s;
    }

    void setTitle(const char*) = delete; // запрещаем эту форму вызова
};

Теперь editor.setTitle("hi"); не скомпилируется, хотя строковый литерал технически мог бы подойти. Зато editor.setTitle(std::string{"hi"}); — ок. Это выглядит чуть строже, но иногда строгость — это просто «будущие баги не пройдут».

Важно: это не универсальная рекомендация «запрещайте const char* всегда». Это пример того, что =delete умеет управлять интерфейсом, а не только жизненным циклом.

5. Пример: TaskRepository, который нельзя копировать

Соберём небольшой, но связный фрагмент «todo‑приложения», где =delete реально защищает дизайн.

Идея такая: у нас есть хранилище задач TaskRepository, внутри которого лежат std::vector<Task> и IdGenerator. По смыслу репозиторий — «центр данных»; копировать его случайно в параметрах или возвращать по значению (не обсуждая перенос) — плохая идея: можно получить две независимые копии списка задач и расхождение состояний.

Сделаем репозиторий non-copyable:

#include <string>
#include <vector>

struct Task {
    int id{};
    std::string title;
    bool done{false};
};

struct IdGenerator {
    int nextId{1};

    IdGenerator(const IdGenerator&) = delete;
    IdGenerator& operator=(const IdGenerator&) = delete;

    int allocate() { return nextId++; }
};

struct TaskRepository {
    std::vector<Task> tasks;
    IdGenerator gen;

    TaskRepository() = default;

    TaskRepository(const TaskRepository&) = delete;
    TaskRepository& operator=(const TaskRepository&) = delete;

    void addTask(const std::string& title) {
        tasks.push_back(Task{gen.allocate(), title, false});
    }
};

Заметьте, мы ничего не «освобождаем» вручную, не пишем деструкторов и не лезем в new/delete. Мы просто говорим: «Копировать нельзя». Это и есть очень здоровая философия современного C++: пусть стандартные компоненты делают тяжёлую работу, а вы описываете смысл и ограничения.

Проверим в main, что всё нормально работает.

#include <iostream>
#include <string>
#include <vector>

int main() {
    TaskRepository repo;
    repo.addTask("Write C++ code");
    repo.addTask("Read compiler errors");

    std::cout << repo.tasks.size() << '\n'; // 2
}

Если кто-то позже попытается сделать TaskRepository copy = repo;, компилятор остановит это на месте — и вы сэкономите часы отладки «почему список задач раздвоился».

6. Когда применять =delete

Когда применять =delete, а когда оставить «как есть»? Хорошая эвристика: =delete нужен там, где неправильное использование типа вероятно и дорого, а правильное использование можно выразить явно.

Удобно держать простую схему:

flowchart TD
    A[Хотите запретить операцию?] --> B{Операция по смыслу некорректна?}
    B -->|Да| C[Запрещаем через =delete]
    B -->|Нет| D{Операция просто нежелательна, но иногда нужна?}
    D -->|Да| E[Не запрещаем: ищем другой дизайн или документируем]
    D -->|Нет| F[Оставляем как есть]

Тут важна «по смыслу некорректна». Например, «создать объект без обязательного id» — это часто некорректно. «Копировать владельца уникального ресурса» — некорректно. «Запретить все конструкторы на всякий случай» — обычно уже перебор.

Явный запрет и запрет «из полей»: два источника non-copyable

Иногда студент видит ошибку компиляции «копирование удалено» и думает: «Но я же не писал =delete!». И это нормально: операция могла стать недоступной автоматически из-за состава полей.

Например, если у вас поле — std::unique_ptr, то копирование всего типа запрещается, потому что unique_ptr сам по себе не копируется. И это полезная защита: «уникальное владение» должно оставаться уникальным.

Мы не будем углубляться в детали, но важно понимать разницу на уровне чтения кода: запрет может быть «явно заявлен» через =delete, а может «подняться» из полей. В обоих случаях результат один: компилятор запрещает операцию. Разница в том, насколько это очевидно из интерфейса.

Во многих командах любят писать =delete даже если поле уже делает тип non-copyable, просто чтобы намерение было видно сразу, не заставляя читателя «проводить расследование по полям».

Мини-таблица: что чаще всего удаляют

Чтобы закрепить, сведём типовые запреты в маленькую таблицу. Она не «закон C++», а практический набор приёмов.

Что запрещаем Как пишется Когда это уместно
Создание «пустого» объекта
T() = delete;
У объекта нет разумного состояния по умолчанию
Копия при создании
T(const T&) = delete;
Копия ломает смысл (уникальность, владение, идентичность)
Копия в существующий объект
T& operator=(const T&) = delete;
Обычно запрещают вместе с копирующим конструктором
Конкретная перегрузка
void f(int) = delete;
Хотим запретить опасный/нежелательный вариант вызова

7. Типичные ошибки при работе с =delete

Ошибка №1: путать =delete и delete p;.
delete p; — это про освобождение динамической памяти. =delete — это про интерфейс и запрет вызова функции. Смешивание этих понятий обычно приводит к странным выводам вроде «запретил конструктор, значит память освободится» (спойлер: нет).

Ошибка №2: запретить только копирующий конструктор, но оставить копирующее присваивание.
Так тип становится «странно полу‑копируемым». В лучшем случае вы получите запутанный интерфейс, в худшем — неожиданное копирование состояния там, где вы его не ждали. Если копирование запрещено по смыслу, чаще всего удаляют обе операции: и T(const T&), и operator=.

Ошибка №3: удалить конструктор по умолчанию и забыть оставить нормальный способ создания.
Это выглядит как дверь без ручки: «объект нельзя создать никак». Запрещая T(), убедитесь, что есть хотя бы один конструктор, который создаёт валидный объект (например, explicit T(int)).

Ошибка №4: пытаться передавать non-copyable тип по значению.
Параметр по значению почти всегда означает копию (или попытку копии). Для non-copyable типов нужно привыкать к const T& для чтения и T& для изменения. И да, сначала это непривычно, но потом вы начинаете видеть интерфейсы «по смыслу», а не «по инерции».

Ошибка №5: использовать =delete «на всякий случай», без формулируемой причины.
Запрет — это часть контракта типа. Если вы не можете объяснить, почему операция запрещена (уникальность, владение, бессмысленное состояние, опасная перегрузка), то велик шанс, что запрет только усложнит использование типа и заставит людей искать обходные пути вместо правильного дизайна.

1
Задача
C++ SELF, 45 уровень, 1 лекция
Недоступна
Пропуск в систему
Пропуск в систему
1
Задача
C++ SELF, 45 уровень, 1 лекция
Недоступна
Текстовый фильтр
Текстовый фильтр
1
Задача
C++ SELF, 45 уровень, 1 лекция
Недоступна
Счётчик турникета
Счётчик турникета
1
Задача
C++ SELF, 45 уровень, 1 лекция
Недоступна
Таблица рекордов
Таблица рекордов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ