templated toString and class inheritance

cc cc at nevernet.com
Sun Dec 25 19:10:13 UTC 2022


`toString` is a vitally important (IMO) method for relaying text 
information about an object to the user, at least during the 
development and debugging stages of an application but quite 
often in production as well.  The basic version usable by the 
standard library potentially allocates (apologies to [this 
thread](https://forum.dlang.org/post/lrunlewuidlxmcrurvme@forum.dlang.org), which got me thinking about this again, for borrowing the class names):
```d
class Animal {
	override string toString() {
		return "Animal()";
	}
}
class Dog : Animal {
	override string toString() {
		return "Dog()";
	}
}
class Bulldog : Dog {
	bool sunglasses;
	override string toString() {
		return format("Bulldog(cool: %s)", sunglasses);
	}
}
```
Now, even the most diehard GC enthusiast should agree that 
situations can arise where you might potentially be calling 
toString hundreds of times per frame in a high-intensity 
application loop, and allocating every single time is highly 
undesirable.  I won't bother discussing issues of caching values 
as the specific use cases and complexity aren't relevant here.  
Fortunately, `std.format` provides a non-[necessarily-]allocating 
alternative:

```d
import std.format;
import std.range.primitives; // Mandatory, see aside
class Animal {
	void toString(W)(ref W writer) if (isOutputRange!(W, char)) {
		writer.formattedWrite("Animal()");
	}
}
class Dog : Animal {
	void toString(W)(ref W writer) if (isOutputRange!(W, char)) {
		writer.formattedWrite("Dog()");
	}
}
class Bulldog : Dog {
	bool sunglasses;
	void toString(W)(ref W writer) if (isOutputRange!(W, char)) {
		writer.formattedWrite("Bulldog(cool: %s)", sunglasses);
	}
}
```

However, we have a problem:
```d
void main() {
	auto animals = [new Animal, new Dog, new Bulldog];
	foreach (animal; animals) {
		animal.writeln;
	}
}
```
```
Animal()
Animal()
Animal()
```

As this function is templated, it cannot be virtual as I 
understand it, and thus we have this problem of determining the 
correct toString to call when the type is known only at runtime.

Current solutions feel somewhat clumsy, and involve for example 
manually registering all such relevant classes and proxying 
toString with a handler function that identifies an object by its 
runtime typeid and looking up the relevant correct function to 
call.  Automatically determining which classes to register can be 
difficult, especially if classes inherit each other across 
multiple files or are added at a later date, and increase 
complexity and programmer responsibility to maintain.

Given how important `toString` is, it would be great if there 
were some kind of special case or path the compiler could use to 
facilitate this (though a more general-purpose solution would 
also be interesting).  I briefly thought "Wouldn't it be nice if 
TypeInfo_Class could store a reference to the class's matching 
toString function that could be called at runtime?", but given 
that it's a template, that's a no go.  Are there better ways to 
handle this?  Have ideas/best practices for working around 
templates and virtual functions or things like that been 
discussed before?  It feels like something that should be so 
simple, conceptually, but despite bordering on a number of D's 
great features there doesn't seem to a be a simple 
fire-and-forget solution.

I have come up with one solution, which I will attach in the next 
post.  Hopefully I haven't missed something completely obvious 
with all this.



More information about the Digitalmars-d mailing list