Both const
and immutable
are type qualifiers. This means that, when applied to any type, they actually create a new type. For example, the following equivalent declarations all create a type that is called const(int)
.
const int x = 10; const(int) y = 11; const z = 12;
In the declaration of z
, the compiler will infer the type. For a basic type such as int
, it makes no difference which syntax is used. However, we'll see shortly that things can be a bit confusing with derived data types, so you may want to get into the habit of using the syntax in the second line when you need to explicitly specify the type. In the rest of this section, we're going to explore the contracts of both const
and immutable
, then take a look at how they apply to the types we've seen so far.
When you declare a variable as immutable
, you are stating unequivocally that it is never, ever going to change throughout the lifetime of the program. This is a very strong contract and the compiler is going to run on the assumption that you really mean what you say. This allows the potential for optimizations that otherwise would not have been possible.
Anything declared as const
is making a guarantee that no data will be modified through that particular reference. This is less strict than the contract of immutable
, because it's still possible for the data to be modified through another, non-const
reference.
Another property of const
and immutable
is that they are both transitive. This means that, if you apply them to a thing, then anything else that is reachable through that thing is also const
or immutable
. You'll get a basic sense of this in this chapter, but you'll achieve full clarity in the next chapter when you learn how to qualify user-defined types.
Applying const
and immutable
to one of the basic types is straightforward. Despite the difference in the contracts, there is effectively no difference between them in this case as the basic types are all value types. When one is assigned to another, the right operand is copied to the left operand. There's no way to change the original value through the new one.
immutable x = 10; const y = 11; const int z; z = 12; // Error
All three variables are completely protected from mutation beyond the point of declaration. x
is forever 10
, y
is forever 11
, and z
is doomed to remain 0
, the value of int.init
.
When applying const
or immutable
to a pointer declaration, you first need to decide if it should apply only to the data, or to the pointer as well.
const(int)* q; // Mutable pointer to const data const(int*) r; // const pointer to const data const int* s; // const pointer to const data
Without parentheses, it's easy to forget which kind of pointer you have. Also, note that none of these guys are initialized; two of them are forever null
. In the first declaration, q
is a mutable pointer. It need not point at the same location forever. It can be assigned a new address at any time, but the data stored at that address can never be mutated. r
and s
are const
pointers, yielding an error if you try to point them somewhere else.
int x = 10; q = &x; // OK: mutable pointer r = &x; // Error: const pointer *q = 11; // Error: const data
Where the contract of const
comes into play is that it's possible mutate const
data through a different pointer where const
is not involved, as shown in this example:
const(int)* cp; int* p; int x = 10; cp = &x; // OK p = &x; // OK
There are two ways to modify x
behind cp
's back. First, you can assign a value to x
directly. Second, you can do it through the pointer p
. const
pointers are often used as function parameters, where the function is promising it won't modify your data through the pointer during the function's lifetime.
The syntax for immutable
is the same. The difference is in the contract. If you have a pointer to immutable data, then the data must be immutable
through all other pointers to that data. The original data must also be immutable
. Consider the following:
immutable(int)* ip; int x = 10; ip = &x; // Error: x is not immutable immutable int y = 11; ip = &y; // OK
You cannot assign ip
a pointer to x
because then there are no guarantees that x
will never be mutated elsewhere in the program. It works for y
because y
is immutable data and cannot be modified directly. That said, you could always cast immutable away. Consider the following two lines:
immutable int y = 10; immutable(int*) py = &y;
We know that it's an error to assign a new address to py
because the pointer, not just the data, is declared as immutable
(the *
is inside the parentheses). Attempting to assign the address of a mutable int
to py
will result in a compiler error, but take a look at this:
writeln(py); int** ppy = cast(int**)&py; int j = 9; *ppy = &j; writeln(py);
This prints two different addresses, showing a successful violation of immutable
's contract. Doing something like this could lead to corrupt data, a segfault, an access violation, or who knows what else. In fact, here's a demonstration of "who knows what else":
immutable int x = 10; int* px = cast(int*)&x; *px = 9; writeln(x);
This snippet takes the same approach used to modify py
above. Compiling and running this, though, actually prints 10
. The compiler, assuming that an immutable int
is never going to be modified, has changed writeln(x)
to writeln(10)
. This is one of the optimizations enabled by immutable
. Taken together, these two examples demonstrate why casting away immutable
is never a good idea. It is undefined behavior and anything can happen.
The same can be said for const
. It's not a violation of the const
contract to modify source data through another reference, but it certainly is a violation to cast const
away and modify it. There's more to the const
story, but it's more clearly demonstrated in the context of user-defined types in the next chapter.
The general idea with arrays is the same as with pointers. For example:
const(int)[] t; // Mutable array, const data const(int[]) u; // const array, const data const int[] v; // const array, const data
Here, none of the metadata associated with u
or v
can be modified through u
or v
. Not the pointer, the length, the capacity, nothing. As with pointers, it's still possible for another, mutable slice to reference the same data. What about t
?
t[0] = 1; // Error: const data t ~= [3,2,1]; // OK: mutable array t.length = 30; // OK: mutable array
The first line fails as one would expect; you can't modify const
data. The last two lines show what it means to have a mutable array. Tacking a slice onto the end of it does not violate the contract, since none of the original elements are mutated and they are all still reachable from their original indexes in the array, no matter that they may have been copied elsewhere during a reallocation. The only modification was to the array metadata. Ditto for the assignment to length
. This all also holds true for immutable
. I trust you can work out how its contract would apply if you swap it in for const
in this example.
You can also apply const
and immutable
to associative arrays and strings. Recall that strings are declared as arrays with immutable
data. If you want to further make the array metadata immutable
or const
, you can do it as with any other data type, such as immutable(string)
, which is effectively immutable(immutable(char)[])
. This is exactly the same as immutable(char[])
.
Types that are passed around by value are implicitly convertible between immutable
, const
, and unqualified; immutable
and unqualified are both implicitly convertible to const
, but not the other way around.
immutable int x = 10; int y = x; // OK: value types immutable(int)* ipx = &x; const(int)* cpx = ipx; // OK: immutable to const const(int)* cpy = &y; // OK: unqualified to const int* px = ipx; // Error
You can think of const
as a bridge between the three. This is particularly helpful with function parameters if the mutability of the source variable is irrelevant.
18.188.98.148