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