Coming from a C# background, I am used to the practice of making structs immutable.
So when I started programming in C++, I tried to do the same with the types that I would usually pass around by value.
I had a simple struct which represents a custom index and simply wraps an integer. Because in C++ the const
keyword is somewhat similar to readonly
in C#, it seemed reasonable to implement my struct like this:
struct MyIndex
{
MyIndex(int value) :
Value(value)
{
}
const int Value;
};
The problem with this approach is that this struct behaves quite differently than an immutable struct in C#. Not just that the Value field of a MyIndex
variable can not be modified, but also any existing MyIndex
variable (local variable or field variable) can't even be replaced with another MyIndex
-instance.
So the following code samples do not compile:
a) Using index as a loop variable.
MyIndex index(0);
while(someCondition)
{
DoWork();
index = MyIndex(index.Value + 1); // This does not compile!
}
b) Modifying a member field of type MyIndex
class MyClass
{
MyIndex Index;
void SetMyIndex(MyIndex index)
{
Index = index; // This does not compile!
}
};
These two samples do not compile, the build error is the same:
error C2582: 'operator =' function is unavailable in 'MyIndex'
What is the reason for this? Why can't the variables be replaced with another instance, despite that they are not const
? And what does the build error mean exaclty? What is that operator =
function?
A struct type is not immutable. Yes, strings are. Making your own type immutable is easy, simply don't provide a default constructor, make all fields private and define no methods or properties that change a field value.
Struct / tuple values are only immutable if they are "let" vs "var". OTOH, enum values are more like true immutable values of functional languages - you can not change enum's associated value, you need a new value with a new associated value to do that.
Usually in C++ you don't declare classes or strict as immutable up-front. Instead you use const when assigning or creating instances. A const object is automatically immutable and enforced by the compiler.
The reason for this is that in C++ after a variable has been created (either a local or field variable) it cannot be "replaced" with another instance, only its state can be altered. So in this line
index = MyIndex(1);
not a new MyIndex instance is created (with its constructor), but rather the existing index variable is supposed to be altered with its copy assignment operator. The missing operator =
function is the copy assignment operator of the MyIndex
type, which it lacks, because it doesn't get automatically generated if the type has a const field.
The reason why it is not generated is that it simply can't be sensibly implemented. A copy assignment operator would be implemented like this (which doesn't work here):
MyIndex& operator=(const MyIndex& other)
{
if(this != &other)
{
Value = other.Value; // Can't do this, because Value is const!
}
return *this;
}
So if we would like our structs to be practically immutable, but behave similarly to immutable structs in C#, we have to take another approach, which is to make every field of our struct private, and mark every function of our class with const
.
The implementation of MyIndex
with this approach:
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;
};
Strictly speaking the MyIndex struct is not immutable, but its state cannot be altered with anything accessible from outside (except its automatically generated copy assignment operator, but that's what we wanted to achieve!).
Now the above code examples compile properly, and we can be sure that our variables of type MyIndex
won't be mutated, unless they are completely replaced by assigning a new value to them.
I think the concepts of immutable objects is largely in conflict with passing by value. If you want to change an an MyIndex
, then you don't really want immutable objects do you? However, if you actually do want immutable objects, then when you write index = MyIndex(index.Value + 1);
you don't want to modify the MyIndex index
, you want to replace it with a different MyIndex
. And that means entirely different concepts. Namely:
std::shared_ptr<MyIndex> index = make_shared<MyIndex>(0);
while(someCondition)
{
DoWork();
index = make_shared<MyIndex>(index.Value + 1);
}
It's a little wierd to see in C++, but in reality, this is how Java works. With this mechanism, MyIndex
can have all const
members, and needs no copy constructor nor copy assignment. Which is right, because it's immutable. Also, this allows for multiple objects to reference the same MyIndex
and know that it will never change, which is a large part of my understanding of immutable objects.
I don't really know how to keep all the benefits listed in http://www.javapractices.com/topic/TopicAction.do?Id=29 without using a strategy similar to this.
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
}
};
It's probably a good idea to replace shared_ptr
with unique_ptr
when a MyIndex
is "owned" by a single clear place/pointer.
In C++ I think the more normal thing is to make mutable structs, and simply make those into const where desired:
std::shared_ptr<const std::string> one;
one = std::make_shared<const std::string>("HI");
//bam, immutable string "reference"
And since nobody likes overhead, we just use const std::string&
or const MyIndex&
, and keep ownership clear in the first place.
Well, you can replace the instance with a brand new instance. It's rather unusual, in most cases people prefer to use the assignment operator.
But if you really wanted C++ to work like C#, you'd do it like this:
void SetMyIndex(MyIndex index)
{
Index.~MyIndex(); // destroy the old instance (optional)
new (&Index) MyIndex(index); // create a new one in the same location
}
But there's no point. The whole advice to make C# structs immutable is highly questionable anyway, and has exceptions wide enough to drive a bulldozer through. Most cases where C++ has advantages over C# fall into one of those exceptions.
Much of the C# advice concerning structs is to deal with limitations on .NET value classes -- there are copied by raw memory copy, they don't have destructors or finalizers, and the default constructor isn't called reliably. C++ has none of these issues.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With