Writing const-correct code in D

Kevin Bealer Kevin_member at pathlink.com
Wed Mar 8 17:50:54 PST 2006


Since people want the benefits of const, I'm showing a way to get them
by following coding conventions.  This requires *no* changes to D.


Also, this is not full "C++ const", only parameter passing and const
methods, which seems to be the most popular parts of the const idea.
It seems like it should require more syntax that C++, but it only
takes a small amount.


When working with types like "int", use "in" - const is not too much
of an issue here.

The same is true for struct, it gets copied in, which is fine for
small structs.  For larger structs, you might want to pass by "in *",
i.e. use "in Foo *".  You can modify this technique to use struct, for
that see the last item in the numbered list at the end.


For classes, the issue is that the pointer will not be modified with
the "in" convention, but the values in the class may be.

: // "Problem" code
:
: class Bar {...}
:
: class Foo {
:   this(Bar b)
:   { x1 = b; }
:
:   this(const_Foo b)
:   {
:     x1 = b.x1.dup;
:   }
:
:   // Modifies this Foo
:   void changeBar(Bar b2)
:   { x1 = b2; }
:
:   // Does not modify this Foo
:   int doesWork() {...}
:
: protected:
:   Bar x1;
: };
:
: // NOTE: changes foo1
: void barfoo(in Foo foo1, in Bar b)
: {
:   foo1.changeBar(b);
: }

We'd like barfoo() not to modify foo1 - we want to guarantee it.

To deal with this, you can write a "const interface" for your class.
I recommend the prefix "const_" so that it looks a little like the C++
version.  This interface definition is quite simple to do.  Note that
a Foo is-a const_Foo, and passing it to a const-Foo interface is
legal.  But modifying it will throw an exception.

NOTE: You don't need any extra method code, except constructors and
optionally the "clone()" method.  What we are doing is SPLITTING the
personality of Foo into two halves - read and write.

: // The read stuff
:
: class const_Foo {
:   this(Bar b)
:   { x1 = b; }
:   
:   this(Foo b)
:   { x1 = b.x1.dup; }
:   
:   // Does not modify Foo
:   int doesWork() {...}
:
:   Foo clone() // how to un-const (optional)
:   {
:      return new Foo(this); // use const->nonconst ctor
:   }
:
: protected:
:   Bar x1;
: };
:
: // The write stuff - can also do read stuff of course.
:
: class Foo : const_Foo {
:   this(Bar b)
:   {
:     const_Foo(b);
:   }
:   
:   this(const_Foo b) // const->nonconst ctor
:   {
:     const_Foo(b.dup);
:   }
:   
:   void changeBar(Bar b2)
:   {
:     x1 = b2;
:   }
: };
:
: // Can only call this with non-const Foo.
: void barfoo(in Foo foo1, in Bar b)
: {
:   foo1.changeBar(b);
: }
: 
: // Can call this with either const_Foo or Foo.
: void barfaa(in const_Foo foo1)
: {
:   int q = foo1.doesWork();
: }

1. In C++, you need to make the same division into const and
non-const, since every method must be labeled as "const" or not
labeled (and thus unusable in a const object).  So there is no extra
"design burden".

2. You can easily change any method's constness by cut/pasting it to
the other class.  All implementation code/data is shared.

3. Relationships are enforced!  If doesWork() calls changeBar(), the
compiler will complain.

4. The class author decides whether "clone()" and the other special
methods are written at all - so if "Bar" is uncloneable for some
reason (i.e. maybe its a File), don't write clone() for Foo, or find a
way to get around copying it.  This work needs to be done in C++ too.

5. Users of const_Foo don't need to know what the editable Foo does.
Their code can't break unless the const_ side is changed.  It's now
very hard to miss the distinction between const/non-const, which is
easy to miss in C++ when writing methods for example.

6. Easy to use as a Copy-On-Write design: If you need to store an
object, and don't know if it is const or not, use a const_Foo
reference.  In the event you need to modify it, you can test whether
it is const with a dynamic cast.  If it is, clone it first!

7. In C++, you can also define distinct const and non-const methods
for a class.  This happens automatically here - the non-const method
(if one exists) just overrides the const one.

8. Finally, for OOD/OOP purists: Although the non-const version is not
really "is-a" const, the relationship still holds once you realize
that const is really a "subtracting" adjective - we could use the
terms readable and read/writeable, where it is easy to see that a
read/writeable think is-a readable thing.

9. You can have "in Foo" parameters and "out const_Foo" without it
being a contradiction.  The first means "I don't want to change what
it points to -- something the caller might also want to know -- but I
might modify it.  The second is a way to return something.  [The
semantics of input and output (argument and return value) are normally
different in OO programming, since one is covariant and the other
contravariant. (This is true in D, right?)]

10. For structs you can do a similar thing:

: // read-write version
: struct X {
:    int opIndex(int i) { ... }
:    int opIndexAssign(int i) { ... }
:    
: private:
:    int[1024] data_;
: };

: // read-only version
: struct const_X {
:    int opIndex(int i) { return impl[i]; }
:    
:    X * clone()
:    {
:       return impl.dup;
:    }
:    
: private:
:    X impl;
: };

If people like this, maybe something along these lines would be useful
for the C++ programmer intro on the D site?  I can make a more
thorough version if so.  If people use this technique, it might be
good for them to follow the same style, i.e. method names.

Kevin





More information about the Digitalmars-d mailing list