D Programming Language 2.0

- Comments
- D
- Search
- Downloads
- Home

Last update Thu Jul 19 00:06:37 2007

# Here A Const, There A Const

by Walter Bright

In a small, experimental program, it's great to benefit from a programming system that's flexible, permissive, and not too pedantic. As the complexity of a program increases, it gets more beneficial to specify the semantics of a declaration in the code itself. Programmers want to carve subdomains in the large application and confine specific state changes to small sections of code. Doing so rids them of long-distance coupling among portions of code that modify the same data. Documentation is unreliable as it is inevitably wrong, misleading, incomplete, out of date, or just plain missing. Of significant utility in this is the notion of constness. C and C++ have added the ability to specify the constness of variables and functions, and it has clearly demonstrated over time that it is popular and useful, and many consider it crucial for developing large programs. In an attempt to simplify, Java dropped const. Its handling of immutable strings and the often-used technique of preemptive copy-out are awkward at best. As a consequence, putting const back into the language has become a favorite indoor sport for industry and academia alike. But C++'s const has a number of important shortcomings, so D took the opportunity to reengineer the concept from top to bottom. This article explores what constness is good for, how C++ constness addresses it, and how D addresses it.

## What Do We Want From Const?

There are a number of benefits that can be derived from knowing something is constant, including benefits to optimization and code generation:

1. Constant data need never be copied! It can be infinitely shared (e.g. via pointers and references) as there is never contention on it. This leads to programs that are both correct and efficient.
2. The most obvious is to just be able to name a manifest constant or string.
3. Constant data can be placed into ROM (read only memory).
4. Const parameters indicate that a function will not modify whatever its arguments refer to, with a direct positive effect on modularity.
5. Constant data indicates that other threads or other aliases to the data cannot modify it.
6. A constant can be propagated and folded, which pulls operations from run time into compile time.
7. Data flow analysis is aided when there's a guarantee that constant data will not change as a side effect of other operations.
8. Constant data can be cached or mirrored in registers without needing to synchronize them with memory.
9. Const reduces the cognitive load on the programmer - by looking at constness in the declaration, he can learn things about whatever uses that declaration without having to slog through that code.

## How Does C++ Const Stack Up?

C++ const comes in two forms: const as a storage class, and const as a type attribute.

Const as a storage class is most useful for declaring manifest constants, such as:

    const int X = 3;

and the language guarantees that `X` will never be anything but 3. `X` can be put into ROM, and the optimizer can reliably replace all rvalues of `X` with 3. Const is a storage class when it applies to the top level type of the declaration.

Const as a type attribute is different. It becomes a type attribute when it does not apply to the top level type of a declaration:

    int x = 3;
    const int *p = &x;

Here the const applies to the int that `p` is pointing to, not `p`. Const as a type attribute means that a read only view of data is taken. It doesn't mean that the data is constant. For example:

    int x = 3;
    const int *p = &x;
    *p = 4;		// error, read-only view

    const int *q = &x;
    int z = *q;	// z is set to 3
    x = 5;		// ok
    int y = *q;	// y is set to 5

`z` is not equal to `y`, even though `*q` is const. This is one instance of the so-called aliasing problem, since while the above snippet is trivial, the existence of such aliases can be very hard to detect in a complex program. It is impossible for the compiler to reliably detect it. This means that the compiler cannot cache 3 in a register and reuse the cached value to replace `*q`, it must go back and actually dereference `q` again.

Consider a function defined as:

    void foo(const int *p);

Ostensibly, it looks like I can safely pass references to my int variables to `foo()` and be assured that `foo()` won't be changing my ints. But that isn't true:

    void foo(const int *p)
    {
        int *q = const_cast<int *>(p);
        *q = 4;
    }

`foo()` has not only cast away the constness, but it has gone and modified my precious int variable, even though `foo()`'s interface promised it would not. Even worse, this is legal and well-defined C++, and must be supported by any C++ compiler. While writing such code is frowned upon by professional C++ programmers, the fact that it is legal means that the compiler is of no help in enforcing it.

So, if someone is doing a code review, and sees a function parameter declared as a pointer to const, he must carefully review all the code in that function, and all the code in functions called by that function that take the parameter as an argument, to see if it is modified or not. This defeats much of the purpose in declaring a parameter as const.

But there are more problems with C++ const. Consider a class:

    class C;
    void foo(const C *p);
    ...
    C c;
    foo(&c);

Does `foo()` modify the contents of `c`? Sure, through the `const_cast`, but there's another legal way. class `C` could have mutable members:

    class C
    {
        public: mutable int x;
    };

    void foo(const C *p)
    {
        p->x = 3;	// ok, C::x is mutable
    }

So our beleagured code reviewer now has to search the definition of `C` for mutable members to see if `foo()` could modify `c`.

The justification for mutable is the concept called logical const, where an object appears to be const to an external viewer, but internally can change. An example would be a class that maintains a cached internal result of an expensive operation. The difficulty with this is two-fold. First, there is no language support at all to ensure that mutable is not used for something other than logical constness. It can be very difficult for a code reviewer to determine if mutable is used correctly in this manner or not. It is impossible to do automated detection of logical constness. Mutable can be and is used for other purposes, and that is completely legal and well-defined C++. Second, having const references to mutable data renders unreliable the ability to rely on const references not being modifiable, which has unfortunate consequences for optimization and writing inherently threadsafe code. It goes back to making it impossible to write generic code that must not modify anything referenced by its parameters.

There's one more problem. Suppose class `C` is the root of a collection, which we'll trivially represent as `T*`:

    class C
    {
        T *q;
    };

and a function `foo()` which reads the collection, and returns some information about it:

    int foo(const C *p);

The `const` only applies to the contents of class `C`, it does not apply to whatever `q` points to:

    int foo(const C *p)
    {
        *p->q = ...;	// ok, we can modify whatever C::q points to
        return 0;
    }

There is no way to specify in `foo()`'s interface that it promises not to modify anything through its parameters. In other words, const is not transitive. This is especially troublesome when attempting to write generic function APIs based on unknown types:

    template<T> int foo(const T *p) { ... }

Without knowing the instantiated type of `T`, it is impossible to know if `foo()` is modifying things through its parameter or not.

To summarize the difficulties with C++ const:

1. Const type attributes do not mean immutable data, they only mean a read-only view of the data. Other references to the same data can modify it at any time.
2. It is legal and defined behavior to cast away const-ness and change the data anyway if the data was originally mutable.
3. Mutable members override the constness of the declaration.
4. Const is not transitive; there is no way to specify the constness of a complex type at the point of use of it.

C++ const is not a good match with the goals listed at the beginning of this article. That means that it's worth a redesign.

## Constness In D

Clearly, there are two distinct meanings to constant - meanings that are routinely conflated. One is that constant data really is constant. It never changes. It's different enough that it needs a different name. In D, this kind of constant is called an invariant.

Invariant data solves the aliasing problem, because even if there are other aliases to the same data, since it is invariant, those references cannot alter the data. The more invariant data a program uses, the easier it is to understand. Invariants form a touchstone, a reference point, for exploring the meaning of the rest of the code. If the value of an invariant does change, it is a clear indication of a severe program bug. It's helpful to have this constraint statically enforced.

The second kind of constant is a readonly view of data, even though the data may be changed through another mutable reference to that same data. This is called const, and is an invaluable modularity aid. One function wants to look at some data; a module has the data, but wants to control changes to it; all they need is a little protocol that allows the function to look at the data, in confidence that it can't change it.

Mutable references can be implicitly converted to const (as in C++). Invariant references can also be implicitly converted to const. But const cannot be implicitly converted to invariant, and neither can mutable references. Essentially, const is a weaker form of invariant because it says: "you can't change this data; someone else may or may not be able to change it."

Const references are usually used in function APIs, where the function is guaranteeing it will not change any data reachable through that const reference.

Which brings up another aspect of const in D - it's transitive. Const in C++ is not transitive, which means one can have a pointer to const pointer to mutable int. To declare a variable that is const at each level, one must write:

    int const *const *const *p;   // C++

The `const` is left associative, so the declaration is a pointer to const pointer to const pointer to const int. Const being transitive in D means that every reference reachable through the const is also const. An entire logical region of an application can be protected by placing only one qualifier. To reflect that, the syntax is different, using constructor-like notation:

    const(int **)* p;	// D

Here the `const` applies to the part of the type that is in parentheses. Note that the syntax makes it impossible to declare things like a pointer to a const pointer to a mutable type. This slight loss in expressiveness is justifiable by the considerable power of transitive protection.

Transitive const solves the problem of specifying function interfaces to data structs that truly are read only, even if they are generic functions dealing with unknown types.

Analogously to const, invariant types are transitive and follow the same syntactical pattern as const.

But there is a need for a constant declaration referencing a mutable type. This is provided with the `final` storage class for declarations:

    int x,y;
    final int* p = &x;
    p = &y;		// error, p is final
    *p = 3;		// ok, *p is mutable

A final declaration, once it is set by its initializer or constructor, cannot ever change its value for its lifetime. Its main purpose is to reduce the effort needed to understand a complex section of code. Final declarations can be mentally separated from variable declarations that are meant to change. Final can also help optimization in a small way by allowing the values to be cached in registers without being concerned about keeping them in sync with the memory value.

Since a final declaration cannot change its value, it is by nature invariant, and the address of it will become a pointer to an invariant:

    final int x = 3;
    invariant(int)* p = &x;
    *p = 4;        // error, *p is invariant
    int y = *p;
    x = 5;         // error, x is final
    int z = *p;    // compiler knows that y==z, and can replace
    	       // (z = *p) with (z = y)

Because a static type system can be a straitjacket, there needs to be a way to circumvent it for special cases. Like C++, D allows the casting away of constness and invariantness. Unlike C++, if the programmer then subverts the const or invariant guarantee and changes the underlying data, then undefined behavior results.

## References

- Const-correctness Wikipedia

## Acknowledgments

Many thanks for Andrei Alexandrescu, Bartosz Milewski, Brad Roberts, David Held, Eric Niebler and many other members of the D community for their major contributions to the design of the new const system.

Many thanks in particular to Andrei Alexandrescu for reviewing this article and making many invaluable suggestions for improving it.

Copyright © 1999-2007 by Digital Mars, All Rights Reserved | Page generated by Ddoc. | Comments