Const-correctness in D

Timur Gafarov
4 min readMar 28, 2020

--

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 barwill 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 halfArrayis 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

constapplied 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 thisin 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() constconst 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
}

--

--