Идиоматический способ объявить неизменные классы С++
Итак, у меня есть довольно обширный функциональный код, где основным типом данных являются неизменяемые структуры/классы. То, как я объявляю неизменность, является "практически неизменным", делая переменные-члены и любые методы const.
struct RockSolid {
const float x;
const float y;
float MakeHarderConcrete() const { return x + y; }
}
Это на самом деле способ "мы должны это сделать" на С++? Или есть лучший способ?
Ответы
Ответ 1
То, как вы предложили, прекрасно, за исключением случаев, когда в коде вам нужно назначить переменные RockSolid, например:
RockSolid a(0,1);
RockSolid b(0,1);
a = b;
Это не сработает, так как оператор копирования будет удален компилятором.
Таким образом, альтернатива заключается в том, чтобы переписать вашу структуру как класс с частными членами данных и только публичные функции const.
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 являются (псевдо) неизменяемыми, но вы все еще можете выполнять задания.
Ответ 2
Я предполагаю, что ваша цель - истинная неизменность - каждый объект, когда он сконструирован, не может быть изменен. Вы не можете назначить один объект над другим.
Самый большой недостаток вашего дизайна заключается в том, что он несовместим с семантикой перемещения, что может сделать функции, возвращающие такие объекты более практичными.
В качестве примера:
struct RockSolidLayers {
const std::vector<RockSolid> layers;
};
мы можем создать один из них, но если у нас есть функция для его создания:
RockSolidLayers make_layers();
он должен (логически) скопировать его содержимое в возвращаемое значение или использовать синтаксис return {}
, чтобы напрямую его построить. Снаружи вам нужно либо сделать:
RockSolidLayers&& layers = make_layers();
или снова (логически) copy-construct. Неспособность переместить-конструкцию будет мешать нескольким простым способам оптимального кода.
Теперь обе эти копии построены, но чем более общий случай выполняется - вы не можете перемещать свои данные из одного именованного объекта в другой, так как С++ не имеет операции "уничтожить и переместить", которая требует переменная вне области действия и использует ее для создания чего-то еще.
И случаи, когда С++ неявно перемещают ваш объект (return local_variable;
например) до уничтожения, блокируются вашими членами данных const
.
В языке, разработанном вокруг неизменяемых данных, он знал бы, что он может "перемещать" ваши данные, несмотря на его (логическую) неизменность.
Один из способов решить эту проблему - использовать кучу и сохранить ваши данные в std::shared_ptr<const Foo>
. Теперь значение const
отсутствует в данных элемента, а скорее в переменной. Вы также можете открывать только factory функции для каждого из ваших типов, которые возвращают выше shared_ptr<const Foo>
, блокируя другую конструкцию.
Такие объекты могут быть скомпилированы с Bar
сохранением элементов std::shared_ptr<const Foo>
.
Функция, возвращающая std::shared_ptr<const X>
, может эффективно перемещать данные, а локальная переменная может переместиться в другую функцию после того, как вы закончите с ней, не имея возможности связываться с "реальными" данными.
Для связанного с ним метода он является idomatic в менее ограниченном С++, чтобы взять такой shared_ptr<const X>
и хранить их в пределах типа оболочки, который делает вид, что они не являются неизменяемыми. Когда вы выполняете mutating operaiton, shared_ptr<const X>
клонируется и модифицируется, а затем сохраняется. Оптимизация "знает", что shared_ptr<const X>
"действительно" a shared_ptr<X>
(обратите внимание: убедитесь, что функции factory возвращают a shared_ptr<X>
приведение к shared_ptr<const X>
, или это на самом деле не так), а когда use_count()
равен 1 вместо этого отбрасывает const
и изменяет его напрямую. Это реализация метода, известного как "копирование при записи".