Как создать неизменяемые структуры на С++?
Исходя из фона С#, я привык к практике создания структур неизменяемых.
Поэтому, когда я начал программировать на С++, я попытался сделать то же самое с типами, которые я обычно передавал по значению.
У меня была простая структура, которая представляет собой настраиваемый индекс и просто обертывает целое число. Поскольку в С++ ключевое слово const
несколько похоже на readonly
в С#, представляется разумным реализовать мою структуру следующим образом:
struct MyIndex
{
MyIndex(int value) :
Value(value)
{
}
const int Value;
};
Проблема с этим подходом заключается в том, что эта структура ведет себя совершенно иначе, чем неизменяемая структура в С#. Не только то, что поле Value переменной MyIndex
не может быть изменено, но и любая существующая переменная MyIndex
(локальная переменная или переменная поля) не может быть заменена другим MyIndex
-instance.
Итак, следующие примеры кода не компилируются:
a) Использование индекса в качестве переменной цикла.
MyIndex index(0);
while(someCondition)
{
DoWork();
index = MyIndex(index.Value + 1); // This does not compile!
}
b) Изменение поля элемента типа MyIndex
class MyClass
{
MyIndex Index;
void SetMyIndex(MyIndex index)
{
Index = index; // This does not compile!
}
};
Эти два примера не компилируются, ошибка сборки одинакова:
ошибка C2582: функция 'operator =' недоступна в 'MyIndex'
В чем причина этого? Почему переменные не могут быть заменены другим экземпляром, несмотря на то, что они не являются const
? И что означает ошибка сборки? Что это за функция operator =
?
Ответы
Ответ 1
Причиной этого является то, что в С++ после создания переменной (либо локальной, либо переменной поля) она не может быть заменена другим экземпляром, только его состояние может быть изменено. Итак, в этой строке
index = MyIndex(1);
не создается новый экземпляр MyIndex (с его конструктором), но существующая переменная индекса должна быть изменена с помощью оператора копирования. Отсутствующая функция operator =
является оператором присваивания копий типа MyIndex
, которого у него нет, потому что он не получается автоматически сгенерированным, если тип имеет поле const.
Причина, по которой он не генерируется, заключается в том, что он просто не может быть разумно реализован. Оператор присваивания копий будет реализован следующим образом (который здесь не работает):
MyIndex& operator=(const MyIndex& other)
{
if(this != &other)
{
Value = other.Value; // Can't do this, because Value is const!
}
return *this;
}
Итак, если мы хотим, чтобы наши структуры были практически неизменными, но ведем себя так же, как и неизменяемые структуры на С#, мы должны использовать другой подход, который должен сделать каждое поле нашей структуры private и отметьте каждую функцию нашего класса const
.
Реализация MyIndex
при таком подходе:
struct MyIndex
{
MyIndex(int value) :
value(value)
{
}
// We have to make a getter to make it accessible.
int Value() const
{
return value;
}
private:
int value;
};
Строго говоря, структура MyIndex не является неизменной, но ее состояние не может быть изменено ничем, доступным извне (кроме его автоматически созданного оператора присваивания копий, но того, чего мы хотели достичь!).
Теперь приведенные выше примеры кода скомпилируются правильно, и мы можем быть уверены, что наши переменные типа MyIndex
не будут мутированы, если они не будут полностью заменены назначением им нового значения.
Ответ 2
Ну, вы можете заменить экземпляр новым экземпляром. Это довольно необычно, в большинстве случаев люди предпочитают использовать оператор присваивания.
Но если вы действительно хотели, чтобы С++ работал как С#, вы бы сделали это следующим образом:
void SetMyIndex(MyIndex index)
{
Index.~MyIndex(); // destroy the old instance (optional)
new (&Index) MyIndex(index); // create a new one in the same location
}
Но нет смысла. Весь совет, чтобы сделать С# structs неизменяемым, в любом случае очень сомнительный, и имеет исключения, достаточно широкие, чтобы пропустить бульдозер. Большинство случаев, когда С++ имеет преимущества перед С#, попадают в одно из этих исключений.
Значительная часть рекомендаций С#, относящихся к структурам, связана с ограничениями классов значений .NET - копируются с помощью необработанной копии памяти, у них нет деструкторов или финализаторов, а конструктор по умолчанию не надежно называет. С++ не имеет этих проблем.
Ответ 3
Я думаю, что концепции неизменяемых объектов в значительной степени противоречат передаче по значению. Если вы хотите изменить a MyIndex
, то вы действительно не хотите, чтобы неизменяемые объекты были у вас? Однако, если вы действительно хотите неизменяемые объекты, тогда, когда вы пишете index = MyIndex(index.Value + 1);
, вы не хотите изменять MyIndex index
, вы хотите заменить его другим MyIndex
. И это означает совершенно разные понятия. А именно:
std::shared_ptr<MyIndex> index = make_shared<MyIndex>(0);
while(someCondition)
{
DoWork();
index = make_shared<MyIndex>(index.Value + 1);
}
Это немного странно видеть на С++, но на самом деле так работает Java. С помощью этого механизма MyIndex
может иметь всех членов const
и не нуждается в конструкторе копирования или присваивании копии. Это правильно, потому что оно непреложное. Кроме того, это позволяет нескольким объектам ссылаться на один и тот же MyIndex
и знать, что он никогда не изменится, что является большой частью моего понимания неизменяемых объектов.
Я не знаю, как сохранить все преимущества, перечисленные в http://www.javapractices.com/topic/TopicAction.do?Id=29, без использования стратегии, аналогичной этой.
class MyClass
{
std::shared_ptr<MyIndex> Index;
void SetMyIndex(const std::shared_ptr<MyIndex>& index)
{
Index = index; // This compiles just fine and does exactly what you want
}
};
Вероятно, стоит заменить shared_ptr
на unique_ptr
, когда MyIndex
"принадлежит" одним ясным местом/указателем.
В С++ я думаю, что более нормальным является создание изменчивых структур и просто встраивание их в const:
std::shared_ptr<const std::string> one;
one = std::make_shared<const std::string>("HI");
//bam, immutable string "reference"
И так как никто не любит накладные расходы, мы просто используем const std::string&
или const MyIndex&
и сохраняем право собственности на первое место.