Про принцип единственной ответственности и правило нуля в C++

Наткнулся недавно на принцип единственной ответственности. А потом на правило нуля, которое вкратце гласит, что классы с пользовательскими copy и move конструкторами, операторами присваивания с copy и с move и пользовательским деструктором должны иметь дело исключительно с правами собственности.

Самый главный вопрос: если класс имеет только права собственности, какие методы кроме той пятерки он может иметь? Например, могу ли я сделать метод get (как в умных указателях) для получения доступа к элементам данных и нарушится ли тогда правило? Буду рад если отошлете на какой нибудь ресурс с примером такого класса

Хочу немного уточнить на каком нибудь примитивном примере.

Пусть есть какой-то простой пример arr_int который будет выделять память под массив интов. Ну и для максимальной простоты без умных указателей:

class ArrInt {
private:
  int *data;
public:
  ArrInt(тут не суть важно) {тут выделение}
  ArrInt(const ArrInt &arr) {тут копирование}
  ArrInt(ArrInt &&) {тут захват ресурсов}
  ArrInt& operator=(const ArrInt &arr) {тут = с копированием}
  ArrInt& operator=(ArrInt &&arr) {тут = с захватом ресурсов}
  ~ArrInt() {тут удаление}

  
  А вот тут начинаются методы по типу сортировки, удаления, добавления
  ...
};

Класс, вроде как, несет 2 ответственности:

  1. Держит выделенную память
  2. Работа элементами массива, ну, какой бы там ни был класс.

И маленькие вопросики

  1. можно ли сделать так через дружественный класс?
class IntBuffer(ну или какое нибудь нормальное название) {
private:
  int *data;
public:
  ArrInt(тут не суть важно) {тут выделение}
  ArrInt(const ArrInt &arr) {тут копирование}
  ArrInt(ArrInt &&) {тут захват ресурсов}
  ArrInt& operator=(const ArrInt &arr) {тут = с копированием}
  ArrInt& operator=(ArrInt &&arr) {тут = с захватом ресурсов}
  ~ArrInt() {тут удаление}

  friend class ArrInt;
}


class ArrInt {
private:
  IntBuffer buff;
public:
  ArrInt(какие то параметры): buff(какие то параметры) {}
  
  методы по типу сортировки удаления добавления
  ...
};

Или вместо дружбы перегрузить оператор для получения элементов или метод для получения?

  1. Или так:
class IntBuffer(ну или какое нибудь нормальное название) {
protected:
  int *data;
public:
  ArrInt(тут не суть важно) {тут выделение}
  ArrInt(const ArrInt &arr) {тут копирование}
  ArrInt(ArrInt &&) {тут захват ресурсов}
  ArrInt& operator=(const ArrInt &arr) {тут = с копированием}
  ArrInt& operator=(ArrInt &&arr) {тут = с захватом ресурсов}
  ~ArrInt() {тут удаление}
}


class ArrInt : public IntBuffer {

public:
  ArrInt(какие то параметры): IntBuffer(какие то параметры) {}

  методы по типу сортировки удаления добавления
  ...
};

Ответы (1 шт):

Автор решения: Chorkov

Права собственности, говорят о способе реализации методов конструирования/копирования/присваивания, а не об остальных методах.

Права собственности подразумевают, что владелец данных всегда один. Никакие изменения, внесенные в данные этого экземпляра класса не должны отражаться в других экземплярах, даже если эти экземпляры получены копированием 1-го экземпляра. Совершенно неважно какие методы реализованы в классе. Неважно, реализованы ли методы присваивания и конструкторы копирования, и т.д..

Этому условию (единственного владельца данных) удовлетворяет std::vector и std::unique_ptr, но не удовлетворяет "сырой" указатель int* и std::shared_ptr, хотя последний обладает полным набором конструкторов и операторов присваивания.

Я бы сказал, что класс всегда должен подразумевать единственного владельца данных, кроме тех случаев, когда это очевидно из названия класса. Т.е. кроме случаев, когда название класса содержит shared, view, mapped, span, subset...


Правило ноля, (советует) не реализовывать самостоятельно операторы присваивания и конструкторы, положившись на операторы присваивания и конструкторы по умолчанию. Подразумевается, что если все члены класса являются единственными владельцами своих данных то конструкторы созданные по умолчанию, также, будут удовлетворять правилу собственности (единственного владельца).

Т.е. рекомендуется не применять голые указатели и shared_ptr в качестве членов класса. (Если только вы не реализуете сейчас shared_** класс, или не используете shared_ptr для константных данных...)


Вам не удастся в чистом виде применить правило нуля, при самостоятельной реализации массива, хотя бы потому, что вы, наверняка, намереваетесь создать один или несколько специальных конструкторов, принимающих длину массив и/или std::initializer_list, по аналогии со стандартным vector. Значит конструкторы копирования (по умолчанию) созданы не будут. Нужно будет написать: ArrInt(const ArrInt&)=default; ... ~ArrInt()=default;. Т.е. нужно использовать правило пяти.


Если вы хотите отделить управление владением и все остальные функции, массива, то, (с точки зрения правило ноля и правила единственной функции) совершенно неважно:

  1. будут ли классы друзьями или нет?
  2. будет ли применена инкапсуляция или наследование?
  3. Будет ли наследование публичным или нет?

Для ответа на эти вопросы используйте другие критерии оптимальной архитектуры.

Если единственной функцией IntBuffer будет владение указателем, то все что нужно для доступа к основной функции, должно быть в публичном интерфейсе класса. Должен ли ArrInt как-то взаимодействовать с IntBuffer, кроме использования его публичного интерфейса? Значит дружба между этими классами не нужна.

Инкапсуляция и наследование, определяется тем что за смысл вы вкладываете в понятие ArrInt. Является ли он владельцем данных, или это деталь его реализации? Появится ли функция для которой важно только что это владелец данных, но неважны все остальные методы ArrInt?...

→ Ссылка