Правило 5 гласит, что если класс имеет объявленный пользователем деструктор, конструктор копирования, конструктор присваивания копирования, конструктор перемещения или конструктор присваивания перемещения, то он должен иметь и остальные 4.
Но сегодня меня осенило: когда вам когда-нибудь понадобится определяемый пользователем деструктор, конструктор копирования, конструктор присваивания копирования, конструктор перемещения или конструктор присваивания перемещения?
Насколько я понимаю, неявные конструкторы/деструкторы прекрасно работают для агрегированных структур данных. Однако классы, которые управляют ресурсом, нуждаются в определяемых пользователем конструкторах/деструкторах.
Однако нельзя ли преобразовать все классы управления ресурсами в агрегированную структуру данных с помощью интеллектуального указателя?
Пример:
// RAII Class which allocates memory on the heap.
class ResourceManager {
Resource* resource;
ResourceManager() {resource = new Resource;}
// In this class you need all the destructors/ copy ctor/ move ctor etc...
// I haven't written them as they are trivial to implement
};
против
class ResourceManager {
std::unique_ptr<Resource> resource;
};
Теперь пример 2 ведет себя точно так же, как пример 1, но работают все неявные конструкторы.
Конечно, вы не можете скопировать ResourceManager
, но если вам нужно другое поведение, вы можете использовать другой умный указатель.
Дело в том, что вам не нужны пользовательские конструкторы, когда умные указатели уже имеют их, поэтому неявные конструкторы работают.
Единственная причина, по которой я хотел бы иметь пользовательские конструкторы, была бы, когда:
вы не можете использовать интеллектуальные указатели в каком-то низкоуровневом коде (я очень сомневаюсь, что это когда-либо имело место).
вы реализуете сами умные указатели.
Однако в обычном коде я не вижу причин использовать пользовательские конструкторы.
Я что-то упустил здесь?
@Peter Это моя точка зрения. Почему нельзя всегда делегировать перемещение/копирование умному указателю?
Что, если вы пишете свой собственный умный указатель?
Это называется «правило нуля».
@Galik Значит, правило 5 устарело? Вы должны следовать правилу нуля?
@Cyrus Не устарело, но предпочтительнее правило нуля. Правило 3/5 по-прежнему применяется, когда у вас есть участник(и), который не управляет своими собственными ресурсами.
@Galik Я не понимаю, когда у вас будет член, который не управляет своими собственными ресурсами. Обычно об этом позаботится умный указатель. Не могли бы вы привести пример этого, пожалуйста (кроме случая, когда вы реализуете интеллектуальный указатель)?
Опять же, предположим, что вы пишете свой собственный интеллектуальный указатель с нуля.
У вас может быть необработанный дескриптор файла на уровне операционной системы для управления (например).
Как насчет реализации почти всех контейнеров? Если вы реализуете вектор, вам необходимо выделить (и, следовательно, освободить) память. Здесь важно правило 3/5. Имейте в виду, что не все используют контейнеры STL. Если вы можете выразить свою программу исключительно путем составления других структур данных, вам не нужно реализовывать ни одну из пяти, и применяется правило 0.
Все, что имеет необычную семантику получения/выпуска.
Ваш пример просто немного надуман, чтобы подчеркнуть суть. Но это не очень хорошо. Скажем, ваш конструктор создает новую таблицу в базе данных, которую деструктор должен завершить. Как бы вы избежали этого с помощью умного указателя?
@TasosPapastylianou Не может std::unique_ptr использовать пользовательское средство удаления в качестве аргумента шаблона?
@Cyrus, возможно, но тогда я бы рассматривал это как делегирование функций, которые логически относятся к обязанностям вашего класса, внешнему, не связанному с ним объекту, просто чтобы избежать правила.
@ TasosPapastylianou Совершенно верно. Я предполагаю, что в некоторых случаях вместо интеллектуальных указателей требуется RAII... Но если бы я использовал указатели для управления ресурсами, я бы определенно использовал интеллектуальные указатели.
Пользовательские удаления зашли так далеко. И, честно говоря, использование указателя для управления чем-то, что не является указателем, вводит в заблуждение, поэтому я бы не рекомендовал это делать. Я очень часто делаю пользовательские удаления, как вы предлагаете, когда это имеет смысл. Но для других целей я пишу специальный менеджер ресурсов (используя правило 3/5), чтобы в будущем я мог использовать этот менеджер ресурсов в других классах, которые следуют правилу нуля.
@cyrus - Вы ошибочно предполагаете, что существующий тип интеллектуальной точки подходит для управления каждым ресурсом, которым вам может понадобиться управлять. Интеллектуальные указатели в стандартной библиотеке C++ управляют определенными типами ресурсов, но не другими. Если вам нужно управлять каким-то другим ресурсом (т. е. стандартная библиотека C++ не предоставляет подходящего класса для управления им), то вы будете писать свой собственный менеджер. Существует множество примеров ресурсов, которые не поддерживаются стандартной библиотекой C++ (например, ресурсы, специфичные для конкретной ОС).
Кстати, обратите внимание, что преобразованный код (агрегатная версия) инициализирует resource
в nullptr
, а не в выделенную память.
«В обычном коде» можно было бы рассматривать как немного... не-PC. Я думаю, что определение «нормального кода» само по себе является докторской степенью (квантовые вычисления берут поклон)
Полное название правила правило 3/5/0.
Это не говорит «всегда предоставлять все пять». В нем говорится, что вы должны предоставить либо три, либо пять, либо ни одного из них.
Действительно, чаще всего самый умный ход — не предоставлять ни один из пяти. Но вы не можете сделать это, если вы пишете свой собственный контейнер, интеллектуальный указатель или оболочку RAII для какого-либо ресурса.
Даже если эта версия правила не является той, которой следует всегда следовать. Есть исключения.
@eeorika Любопытно, какие есть исключения? Я не думаю, что видел их.
Допустим, вам нужен указатель, указывающий на элемент. Если вы копируете объект, вам необходимо обновить этот указатель. Таким образом, вам нужен собственный (или удаленный) конструктор копирования и оператор присваивания. Вам не нужен деструктор.
@HolyBlackCat, у меня есть класс, представляющий собой оболочку C++ для соединения с базой данных SQLite. У него есть деструктор (поэтому соединение закрывается при уничтожении объекта), но для правильного функционирования требуется, чтобы объект соединения был уникальным: вызов конструктора копирования, оператора присваивания или чего-либо еще, что создало бы вторую оболочку объекта, является ошибкой. такое же соединение.
@Mark Это требует =delete
операций копирования, которые IMO считает их предоставлением для целей правила 3.
Однако в обычном коде я не вижу причин использовать пользовательские конструкторы.
Предоставляемый пользователем конструктор позволяет также поддерживать некоторый инвариант, поэтому он ортогонален правилу 5.
Как, например,
struct clampInt
{
int min;
int max;
int value;
};
не гарантирует, что min < max
. Таким образом, инкапсуляция данных может обеспечить эту гарантию.
совокупность подходит не для всех случаев.
когда вам когда-нибудь понадобится определяемый пользователем деструктор, конструктор копирования, конструктор присваивания копирования, конструктор перемещения или конструктор присваивания перемещения?
Теперь о правиле 5/3/0.
Действительно правило 0 должно быть предпочтительным.
Доступные смарт-указатели (я включаю контейнер) предназначены для указателей, коллекций или Lockables.
Но ресурсы не являются обязательными указателями (может быть дескриптор, скрытый в int
, внутренние скрытые статические переменные (XXX_Init()
/XXX_Close()
)), или могут требовать более сложной обработки (как для базы данных, автоматической фиксации в конце области или отката в случае исключений). ), поэтому вам нужно написать свой собственный объект RAII.
Вы также можете написать объект RAII, который на самом деле не владеет ресурсом, например, как TimerLogger
(записать прошедшее время, используемое «областью действия»).
Еще один момент, когда вам обычно приходится писать деструктор, — это для абстрактного класса, так как вам нужен виртуальный деструктор (а возможная полиморфная копия выполняется виртуальным clone
).
Спасибо, что разъяснили, что управление ресурсами != указатель. Мне никогда не приходило в голову, что вы можете использовать int для управления ресурсами... Теперь я понимаю, почему в этом случае нужен RAII.
Наличие хороших инкапсулированных концепций, которые уже следуют правилу пяти, действительно гарантирует, что вам придется меньше беспокоиться об этом. Тем не менее, если вы окажетесь в ситуации, когда вам нужно написать какую-то пользовательскую логику, она все равно будет работать. Некоторые вещи, которые приходят на ум:
Кроме того, я обнаружил, что как только у вас будет достаточно композиции, уже неясно, каким будет поведение класса. Доступны ли операторы присваивания? Можем ли мы скопировать конструкцию класса? Поэтому соблюдение правила пяти, даже с = default
в нем, в сочетании с -Wdefaulted-function-deleted as error помогает понять код.
Чтобы рассмотреть ваши примеры поближе:
// RAII Class which allocates memory on the heap.
class ResourceManager {
Resource* resource;
ResourceManager() {resource = new Resource;}
// In this class you need all the destructors/ copy ctor/ move ctor etc...
// I haven't written them as they are trivial to implement
};
Этот код действительно может быть красиво преобразован в:
class ResourceManager {
std::unique_ptr<Resource> resource;
};
Однако теперь представьте:
class ResourceManager {
ResourcePool &pool;
Resource *resource;
ResourceManager(ResourcePool &pool) : pool{pool}, resource{pool.createResource()} {}
~ResourceManager() { pool.destroyResource(resource);
};
Опять же, это можно сделать с помощью unique_ptr
, если вы дадите ему собственный деструктор.
Однако, если ваш класс теперь хранит много ресурсов, готовы ли вы платить дополнительную цену за память?
Что, если вам сначала нужно взять блокировку, прежде чем вы сможете вернуть ресурс в пул для повторного использования? Вы возьмете эту блокировку только один раз и вернете все ресурсы или 1000 раз, когда будете возвращать их по одному?
Я думаю, что ваши рассуждения верны, наличие хороших типов интеллектуальных указателей делает правило 5 менее актуальным. Однако, как указано в этом ответе, всегда есть случаи, когда вам это понадобится. Поэтому называть его устаревшим может быть слишком далеко, это немного похоже на знание того, как выполнять итерацию, используя for (auto it = v.begin(); it != v.end(); ++it)
вместо for (auto e : v)
. Вы больше не используете первый вариант, вплоть до того, что вам нужно вызвать «стирание», когда это вдруг снова становится актуальным.
Как уже отмечалось, полное правило — это Правило 0/3/5; обычно реализуйте 0 из них, а если вы их реализуете, реализуйте 3 или 5 из них.
Вы должны реализовать операции копирования/перемещения и уничтожения в нескольких случаях.
Ссылка на себя. Иногда части объекта ссылаются на другие части объекта. Когда вы копируете их, они будут наивно ссылаться на другой объект, из которого вы скопировали.
Умные указатели. Есть причины для реализации более интеллектуальных указателей.
В более общем смысле, чем интеллектуальные указатели, типы владения ресурсами, такие как vector
s, optional
или variant
s. Все эти типы словарей позволяют пользователям не заботиться о них.
Более общий, чем 1, объекты, идентичность которых имеет значение. Объекты, которые имеют внешнюю регистрацию, например, должны заново зарегистрировать новую копию в хранилище регистров, а при уничтожении должны отменить регистрацию.
Случаи, когда вы должны быть осторожны или причудливы из-за параллелизма. Например, если у вас есть шаблон mutex_guarded<T>
и вы хотите, чтобы его можно было копировать, копирование по умолчанию не работает, так как у оболочки есть мьютекс, а мьютексы нельзя копировать. В других случаях может потребоваться гарантировать порядок некоторых операций, выполнить сравнение и установку или даже отследить или записать «собственный поток» объекта, чтобы определить, когда он пересекает границы потока.
Это правило часто неправильно понимают, потому что оно часто оказывается чрезмерно упрощенным.
Упрощенная версия выглядит так: если вам нужно написать хотя бы один из (3/5) специальных методов, вам нужно написать все (3/5).
Актуальное полезное правило: класс, отвечающий за ручное владение ресурсом, должен: иметь дело исключительно с управлением владением/сроком жизни ресурса; чтобы сделать это правильно, он должен реализовать все 3/5 специальных членов. В противном случае (если ваш класс не имеет ручного владения ресурсом) вы должны оставить все специальные члены неявными или заданными по умолчанию (правило нуля).
Упрощенные версии используют эту риторику: если вам нужно написать один из (3/5), то, скорее всего, ваш класс вручную управляет владением ресурсом, поэтому вам нужно реализовать все (3/5).
Пример 1: если ваш класс управляет получением/высвобождением системного ресурса, он должен реализовать все 3/5.
Пример 2: если ваш класс управляет временем жизни области памяти, то он должен реализовать все 3/5.
Пример 3: в вашем деструкторе вы ведете журнал. Причина, по которой вы пишете деструктор, заключается не в том, чтобы управлять ресурсом, которым вы владеете, поэтому вам не нужно писать другие специальные члены.
В заключение: в пользовательском коде вы должны следовать правилу нуля: не управлять ресурсами вручную. Используйте оболочки RAII, которые уже реализуют это для вас (например, интеллектуальные указатели, стандартные контейнеры, std::string
и т. д.).
Однако, если вам нужно вручную управлять ресурсом, напишите класс RAII, который отвечает исключительно за управление временем жизни ресурса. Этот класс должен реализовать все (3/5) специальных членов.
Полезно прочитать об этом: https://en.cppreference.com/w/cpp/language/rule_of_three
Если каждый экземпляр класса управляет своим собственным экземпляром ресурса, а также необходимо копировать (или перемещать) этот ресурс между экземплярами класса, тогда применяется правило пяти. Если ваш класс делегирует это копирование/перемещение соответствующему интеллектуальному указателю (который, в свою очередь, должен обрабатывать копирование/перемещение ресурса и, следовательно, соответствовать правилу пяти), тогда ваш класс может соответствовать правилу нуля.