Understanding and Using Const in D

One of the main goals in D is to use the compiler to enforce restrictions set up by the programmer. To this end, there are several concepts which allow the programmer to define what is and is not possible. These restrictions help the programmer (and the compiler) reason about how code can legally be constructed, and allows interesting things like lock-free programming, or provable pure functions.

The main mechanism to enforce semantics is collectively referred to as "const" by the community. Wait a minute, there's more to D's enforcement system than const, isn't there? Yes, but because a) const in C++ is the precursor to what grew into D's enforcement system, and b) const was the main focus of debate in the early days that D2 was forming the concepts of how the system will work.

We'll go through the different types of qualifiers, where I'll briefly explain how they work, and then I'll talk about some of the stickier issues surrounding them. Finally, I will discuss my attempts (so far) to use the const system in my existing project dcollections, and what is missing from the system.

A Tale of Two Qualifiers

In D, there are two major qualifiers. The first (and default) is mutable. There is no keyword for this, everything that *doesn't* have a type qualifier is mutable. Mutable means the variable holding this type can change. In other words, it's just a "normal" variable.

e.g.:

int i = 5;
i = 4; // we can change i, it's mutable

The opposite of mutable, is immutable. This means that nothing can change it. To make a type immutable, we simply use the immutable type modifier to alter it:

immutable(int) i = 5;
i = 4; // Error, cannot modify immutable data!

As you can see from the example, immutable data cannot be modified. One interesting property about D immutability (and the property which makes it so useful) is that immutable data with references can only refer to other immutable data. This means that any type which is qualified with immutable must only point at data which is also immutable:

struct S
{
   int *p;
}

int i;
immutable(S) s = immutable(S)(&i); // error, s is immutable and must only point at immutable data

Two Wild and Crazy Modifiers

Given that all data is either immutable or mutable, how do we write functions that can deal with both? Enter the const and inout qualifiers.

const serves as a placeholder for the mutable or immutable qualifier. Because both types of data can be viewed under the guise of const, there are certain properties that must hold:

  1. const must be transitive, since immutable is transitive.
  2. const cannot guarantee the data will never change, since the data could be mutable.

Surprisingly, these rules work quite well for defining functions. In English, the promise of const is simply "I will not modify this data via this reference". Because of these rules, both mutable and immutable data can be implicitly cast to const.

inout, like const, is a placeholder, and does not allow modification of data. But unlike const, it can morph back into the original qualifier passed into it at the scope where a type became inout. This allows modularization of code without bludgeoning the data into a const type. I'll give a simple example for inout:

struct S
{
   int *p;
}

inout(int)* foo(inout(S)* s)
{
   return s.p;
}

The type returned by foo depends on the type passed into foo. If foo is passed an immutable(S)*, an immutable(int)* is returned. Note also that foo is not a template, inout acts similarly to const, accepting any qualifier (mutable, immutable, const, or even inout) with one function generated.

To read more about inout, check out Walter Bright's article on the subject, Type Qualifiers and Wild Cards.

Member Confusion

Probably the most confused aspect of these type qualifiers is how it applies to member functions. To understand what a 'const member function' actually is, it helps to understand why it was created.

Let's assume we have the same struct S we've been using in the examples so far:

struct S
{
   int *p;
}

We can use the const wildcard to define a function that takes both a mutable or immutable version of the struct, without violating the guarantees of immutable:

int foo(ref const(S) s)
{
   return *s.p;
}

// usage
S s;
immutable(S) s2;
foo(s); // works
foo(s2); // works too!

But that foo(s) is so 1970's! OO programming is all the rage these days, so why not move that into the struct itself and make it a member function?

struct S
{
   int *p;
   int foo()
   {
      return *p;
   }
}

// usage
S s;
immutable(S) s2;
s.foo(); // works
s2.foo(); // Error!

Oops! we lost our wildcard because now, the reference to S parameter in foo has become invisible! So immutable S instances cannot call foo, because the compiler cannot guarantee its safety.

The solution is to create a 'const member function', and most C++ coders will recognize this syntax:

struct S
{
   int *p;
   int foo() const
   {
      return *p;
   }
}

The const qualifier listed after the function signature is simply the arbitrary spot chosen to list the qualifiers that apply to the hidden 'this' reference. In other words, a 'const member function' does not 'apply const to the function' as the name suggests. Rather, it simply applies const to the hidden reference.

Note that in D, you can also apply type qualifiers to the hidden 'this' pointer by placing at the front of the function (a very confusing location, I don't recommend this), or by using any group application of attributes:

The same technique is used to apply ''any'' type qualifiers to the hidden parameter.  In D, those qualifiers are `const`, `immutable`, `inout`, and `shared`.  So the next time you see a type qualifier listed for a function, recognize that it does not actually apply to the function, but simply the function's hidden reference.

== Tail Const ==

== Practical Application (dcollections) ==

== What's Left? ==

== Conclusion ==