Writing const-correct code in D
xs0
xs0 at xs0.com
Thu Mar 9 06:49:15 PST 2006
Kevin Bealer wrote:
> 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
OK, while it might work, your approach has several problems:
- applicability
- there is no support for arrays and pointers to primitive types;
especially arrays are a problem
- coding efficiency
- maintenance of an additional class is bad, having to write
a wrapper for structs is even worse
- while arbitrarily flexible, the approach is also error-prone,
like all manual methods
- runtime efficiency
- say you have a struct in your object; you can't return a readonly
pointer to it, so you either have to heap-allocate a new struct,
or make the wrapper contain a pointer, incurring double
dereferencing
- if you do use a pointer, you have to heap-allocate the wrapped
struct, otherwise you can't pass the read-only version around
freely
- two classes means two vtbls, two TypeInfos, ...
Any thoughts on that? :)
xs0
More information about the Digitalmars-d
mailing list