Const-correctness in D
const
qualifier is familiar to everyone, it is present in many modern C-like languages and used in many different contexts. In the most general sense, it means an immutable variable that can only be initialized once. In dynamically typed languages const
usually means exatly that, but in statically typed world, including D as well, there are different types of immutability, and understanding them is cruisal to write reliable software.
Why do we need it?
By executing a computer program we utilize a processor to perform computations. If your computation is something more complex than a simple mathematic formula, then your program will have a state — that is, an intermediate data in memory that is kept for later use. All the comlexity of a program usually comes from state management and memory safety, because raw memory is just a set of addressable bytes without any intrinsic structure. To reduce this complexity and prevent invalid behaviour, one can define a data model and constrain it with rules — example of such set of rules is a type system, which defines data types and their properties. Given a static type system in a language, a compiler can do semantic analysis and reject a program that tries to do something illegal.
Const-correctness is an additional restriction that improves semantic analysis. A simple program in a statically-typed language can get away without const-correctness. But in a complex system where state is shared between multiple asynchronous users, or even parallel threads, you want a way to minimize side effects, so that shared state can’t be accidently modified when it is wrong to do so. const
is one such way.
const in D
In C-like languages const
is an additional rule in a type system that basically says “any variable of this type can’t be changed”. D also has const
qualifier, and it is very important to understand its differencies from const
in C++. In C++ it is not transitive, meaning that const
objects can have mutable members. In D const
is transitive. Once it is applied to a type, it applies to every member of that type:
class Foo
{
int x = 10;
}const(Foo) foo = new Foo();
foo.x = 5; // illegal!
Also there’s syntactical distinction between const
pointers and mutable pointers to const
:
const int*
—const
pointer to const int
const(int*)
— const
pointer to const int
const(int)*
— mutable pointer to const int
const vs immutable
D also has immutable
qualifier, and this often cause confusion. Immutable type means that object of that type, once created, cannot be changed. const
type means that object cannot be changed by an identifier of that type. It may, however, be changed by another, mutable identifier. So const
can be seen as an interface that ensures read-only access.
When to use const
, and when immutable
? Consider the following example:
class Foo
{
int x = 10;
}int bar(Foo foo1, Foo foo2)
{
return foo1.x + foo2.x;
}void main()
{
immutable(Foo) foo1 = new Foo();
Foo foo2 = new Foo();
bar(foo1, foo2);
}
This program will not compile, because foo
is immutable. The solution is to use const
:
int bar(const(Foo) foo1, const(Foo) foo2)
{
return foo1.x + foo2.x;
}
This way bar
will accept both mutable and immutable parameters.
Another interesting question: is it possible to remove constness by casting it away? In reliable way, no. Being a system language, D won’t stop you from breaking rules — compiler allows casting immutable
or const
type to mutable. This is only for compatibility with C libraries. It doesn’t mean that you can safely change the data after such cast (it is undefined behaviour). As always, unsafe features like that should be used very carefully. Never do something like this:
const int x = 10;
int* px = cast(int*)&x;
*px = 5;
const vs inout
Another type qualifier in D which is somewhat obscure for most beginners is inout
. Imagine your function has to return a type with the same mutability that was passed as a parameter:
auto halfArray(const(int)[] a)
{
return a[$/2..$];
}immutable(int)[] arr1 = [4, 5, 7, 8];
immutable(int)[] arr2 = halfArray(arr1);
This will not compile, because the inferred return type of halfArray
is const(int)[]
, not immutable(int)[]
. But it will work if you replace const
with inout
:
auto halfArray(inout(int)[] a)
{
return a[$/2..$];
}
The same function will also work with const
types:
const(int)[] arr1 = [4, 5, 7, 8];
const(int)[] arr2 = halfArray(arr1);
…and, of course, with mutable ones:
int[] arr1 = [4, 5, 7, 8];
int[] arr2 = halfArray(arr1);
const methods
const
applied to a method’s return type is a special case:
class Foo
{
int prop() const
{
return _prop;
} protected int _prop = 0;
}
This means that implicit this
in a method is const
, and, consequently, its properties cannot be changed. So it is illegal to do the following:
class Foo
{
int prop() const
{
_prop = 8;
return _prop;
} protected int _prop = 0;
}
const
qualifier can be applied only to methods, but not to free functions. They still can return const
type though, so the following declarations have different meaning:
const int foo()
or int foo() const
— const
method that returns int
const(int) foo()
— a method or function that returns const(int)
You can have a const
method that returns const
type:
import std.string;class Foo
{
const(char*) prop() const
{
return _prop.toStringz();
} protected string _prop = "something";
}
ref const
It is possible to have ref const
identifiers. They are useful, for example, in foreach
statement to ensure that elements of an aggregate are not changed by reference:
void foo(ref const(int) v)
{
v = 0; // illegal
}int[] arr = [1, 2, 3, 4, 5];foreach(ref const v; arr)
{
foo(v);
v = 0; // illegal
}