Tags

, ,

I recently fixed a defect that I felt the compiler could have caught. Some initialization code was not being run as I was calling a protected function instead of the correct public one. With some simple additional access modifiers it would be easy to prevent this type of error.

The problem

My code had a fairly common structure: a public function which wraps an internal protected one. In my case it was part of Fuse’s layout code in the Element class:

1
2
3
4
5
6
public float2 ArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet) {
    // ...common setup code...
    var sz = OnArrangeMarginBox(position, availableSize, availSet);
    // ...final layout code...
    return sz;
}

Where:

1
protected abstract float2 OnArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet);

The problem lies in the protected modifier on the OnArrangeMarginBox. This is needed so the base class can call the derived implementations, but it also means all those implementations can call this function on other instances.

The problem surfaces for me since I have a container class including another element. During measuring it needs to measure the other element and I did this by accident:

1
2
3
protected override float2 OnArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet) {
    return MyContent.OnArrangeMarginBox(poistion, availableSize, availSet);
}

I called OnArrangeMarginBox instead of ArrangeMarginBox, thus getting invalid results!

The simple solution

When I marked the OnArrangeMarginBox function as protected I really wanted to say that only the current instance can call that function. Another instance of the same type shouldn’t be able to call it. It’s actually a very common desire for protected functions and properties; they are meant to be available only to the current instance.

1
protected(instance) abstract float2 OnArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet);

This would have caught my error quickly, since MyContent.OnArrangeMarginBox is no longer allowed; MyContent is not this instance thus I can’t call instance protected methods on it.

I’ve looked at a lot of my code in several projects and I’m starting to wonder if maybe instance protected isn’t the more reasonable default. Other than a few comparators it’s rare that I actually want my protected fields accessible by other instances. I’m sure I’ve seen at least one language where this is the case, but I can’t remember which.

The complex solution

I’m tempted to go one step further here. OnArrangeMarginBox should actually not be called by anything other than the ArrangeMarginBox function. It’s not really a standalone function: it’s a component of the public function.

I’d like to say:

1
callable_by(ArrangeMarginBox) abstract float2 OnArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet);

Where callable_by(ArrangeMarginBox) specifies that only a single function may call this one.

This modifier would have prevented several defects I’ve had over the years. It’d block another situation I had with layout code as well. ArrangeMarginBox has a counterpart ArrangePaddingBox, along with it’s OnArrangePaddingBox partner. There are a lot of types that implement layout, and it’s an easy mistake to call OnArrangePaddingBox when we actually meant OnArrangeMarginBox. Markers like callable_by can help prevent this type of error.

Strictness over convention

A lot of projects use conventions to indicate extended modifiers, such as using a trailing underscore, different casing, or other naming patterns. I would much prefer that my compiler can validiate my code rather than me manually tracking such conventions. The compiler is just better at these fine details.

There’s always a balance that has to be reached with adding more modifiers. We want to write safe code, but we don’t want to lose the ease of programming new code. I think OOP is missing a lot of flow control modifiers though, making it confusing to write type hierarchies. I’d welcome the addition of modifiers that help me better manage my virtual functions.