Идиоматический способ объявления неизменяемых классов C ++

Итак, у меня есть довольно обширный функциональный код, в котором основным типом данных являются неизменяемые структуры / классы. Способ, которым я объявлял неизменяемость, «практически неизменяем», делая переменные-члены и любые методы константными.

struct RockSolid {
   const float x;
   const float y;
   float MakeHarderConcrete() const { return x + y; }
}

Действительно ли так «мы должны это делать» в C ++? Или есть способ лучше?


person BlamKiwi    schedule 11.11.2014    source источник
comment
Это во многом зависит от вашей желаемой концепции неизменяемого. Рассмотрим строки Java и C #. Они неизменны, но могут быть назначены.   -  person Cheers and hth. - Alf    schedule 11.11.2014
comment
const члены данных имеют то преимущество, что вы получите ошибку, если забудете инициализировать такой член базового типа. Недостатком является то, что вы не можете присвоить переменным типа.   -  person Cheers and hth. - Alf    schedule 11.11.2014
comment
Основным недостатком является то, что вы не можете перемещать данные из объекта, если его члены const, поэтому я бы предпочел сделать их частными и вместо этого предоставлять только const геттеры и методы, как предлагает oikosdev.   -  person Joe    schedule 22.11.2014


Ответы (2)


Предложенный вами способ идеален, за исключением случаев, когда в вашем коде вам нужно назначить переменные RockSolid, например:

RockSolid a(0,1);
RockSolid b(0,1);
a = b;

Это не сработает, поскольку оператор присваивания копии был бы удален компилятором.

Таким образом, альтернативой является переписывание вашей структуры как класса с частными членами данных и только общедоступными константными функциями.

class RockSolid {
  private:
    float x;
    float y;

  public:
    RockSolid(float _x, float _y) : x(_x), y(_y) {
    }
    float MakeHarderConcrete() const { return x + y; }
    float getX() const { return x; }
    float getY() const { return y; }
 }

Таким образом, ваши объекты RockSolid являются (псевдо) неизменяемыми, но вы по-прежнему можете выполнять назначения.

person chrphb    schedule 11.11.2014
comment
Семантика, которую я особенно хочу, по крайней мере для этого проекта, такая же, как и в функциональных языках 900 фунтов, таких как Haskell, где любое новое состояние должно быть явно (тип) построено. - person BlamKiwi; 11.11.2014
comment
Вы можете рассмотреть возможность использования неизменяемого значения и идиомы изменчивого компаньона. - person Martin Moene; 21.11.2014
comment
@MartinMoene: Эта идиома по-прежнему строит объекты значений, не назначаемые копированием и не присваиваемые перемещением (или полагается на кучу). Решение oikosdev не страдает этим недостатком и не требует (сопутствующего) шаблона. Просто работает ... Зачем все усложнять? - person paercebal; 24.11.2014
comment
Зачем предлагать использовать class? Подойдет даже struct с private переменными-членами! - person CinCout; 10.12.2015
comment
@chrphb: Вот мое утверждение - мы сделали это, потому что для того, чтобы тип был CopyAssignable, он должен иметь общедоступный оператор присваивания копии. Я правильно думаю? - person jack_1729; 11.11.2017
comment
Я прочитал в этом сообщении dzone.com/articles/how -to-create-an-immutable-class-in-java, что неизменяемые классы не могут быть унаследованы никаким другим классом, и это делается с помощью ключевого слова final в Java. При поиске я обнаружил, что ключевое слово final также присутствует в C ++; чтобы подтвердить, не следует ли нам использовать это и в реализации неизменного класса на C ++? - person Utkarsh; 10.06.2019
comment
struct и class - это одно и то же, но одно значение по умолчанию - public:, а другое - private:. Предложение использовать class не имеет значения. Другие языки, такие как C # или D, имеют семантическую разницу. - person Eljay; 08.10.2019

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

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

Например:

struct RockSolidLayers {
  const std::vector<RockSolid> layers;
};

мы можем создать один из них, но если у нас есть функция для его создания:

RockSolidLayers make_layers();

он должен (логически) скопировать свое содержимое в возвращаемое значение или использовать синтаксис return {} для его непосредственного построения. Снаружи нужно либо сделать:

RockSolidLayers&& layers = make_layers();

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

Теперь обе эти копии-конструкции опущены, но сохраняется более общий случай - вы не можете перемещать свои данные из одного именованного объекта в другой, поскольку C ++ не имеет операции удаления и перемещения, которая одновременно берет переменную за пределы области видимости и использует ее для построения чего-то еще.

И случаи, когда C ++ неявно перемещает ваш объект (например, return local_variable;) до уничтожения, блокируются вашими const членами данных.

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

Один из способов решить эту проблему - использовать кучу и хранить данные в std::shared_ptr<const Foo>. Теперь constness находится не в данных элемента, а в переменной. Вы также можете предоставлять только фабричные функции для каждого из ваших типов, которые возвращают указанное выше shared_ptr<const Foo>, блокируя другие конструкции.

Такие объекты могут быть составлены с Bar хранением std::shared_ptr<const Foo> членов.

Функция, возвращающая std::shared_ptr<const X>, может эффективно перемещать данные, а состояние локальной переменной может быть перемещено в другую функцию после того, как вы закончите с ней, без возможности возиться с реальными данными.

Что касается родственной техники, то в менее ограниченном C ++ является идеальным брать такие shared_ptr<const X> и хранить их в типе оболочки, который делает вид, что они не являются неизменяемыми. Когда вы выполняете операцию изменения, shared_ptr<const X> клонируется и модифицируется, а затем сохраняется. Оптимизация знает, что shared_ptr<const X> на самом деле shared_ptr<X> (примечание: убедитесь, что фабричные функции возвращают shared_ptr<X> приведение к shared_ptr<const X>, или это на самом деле неверно), и когда use_count() равно 1, вместо этого отбрасывает const и изменяет его напрямую. Это реализация метода, известного как копирование при записи.

Теперь, когда С ++ развивается, появляется больше возможностей для исключения. Даже C ++ 23 будет иметь более продвинутые возможности. Elision - это когда данные не перемещаются и не копируются логически, а имеют два разных имени: одно внутри функции, а другое снаружи.

Надеяться на это по-прежнему неудобно.

person Yakk - Adam Nevraumont    schedule 24.11.2014
comment
или std::unique_ptr<const X>, который может быть возвращен по значению через std::move(). - person Vorac; 21.03.2017