www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - templated toString and class inheritance

reply cc <cc nevernet.com> writes:
`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.
Dec 25 2022
next sibling parent cc <cc nevernet.com> writes:
On Sunday, 25 December 2022 at 19:10:13 UTC, cc wrote:
 I have come up with one solution, which I will attach in the 
 next post.
Here is what I am playing with currently. I don't know if it's the best way to handle the situation. Redundant checks end up being made. Given that there are two levels of indirection that need to be considered (What OutputRange template is being passed around? and What is the runtime type of the object being written?), I had to get a little creative, I think. Since we can't cleanly anticipate which matches of toString(W)..isOutputRange will be used (and could be a voldemort type) I have incorporated the registering of classes directly into the toString handler, but this can still potentially fail if derived classes are spread into different files and not called first. ```d module util.tostringmaster; import std.string; import std.traits; import std.format; import std.range.primitives; // TODO: Make sure toString template is actually compatible with the desired W // Proper error handling/fallback if matching toString doesn't exist abstract final class ToStringMaster { private: static abstract class ToStringBase {} static abstract class ToStringWriter(W) : ToStringBase { void writeWith(ref W writer, Object obj); } static final class ToStringHolder(W, T) : ToStringWriter!W { override void writeWith(ref W writer, Object obj) { T t = cast(T) obj; t.toString(writer); } } static ToStringBase[string][string] table; static void[0][string] writerTable; static void registerAll(W, string MOD)() { enum WNAME = fullyQualifiedName!W; enum MNAME = WNAME~":"~MOD; //pragma(msg, "REGISTERALL "~MNAME); if (MNAME in writerTable) return; writerTable.require(MNAME); { mixin(`import `~MOD~`;`); mixin(`alias ALL = __traits(allMembers, `~MOD~`);`); static foreach (sym; ALL) {{ mixin(`alias SYM = __traits(getMember, `~MOD~`, "`~sym~`");`); static if (is(SYM == class)) { alias CLASS = SYM; alias TOSTRING = __traits(getMember, CLASS, "toString"); static if (__traits(isTemplate, TOSTRING)) { register!(W, CLASS); } } }} // Explicit class registration //register!(W, Animal); //register!(W, Dog); //register!(W, Bulldog); } } static void register(W, T)() { enum WNAME = fullyQualifiedName!W; enum TNAME = fullyQualifiedName!T; table.require(WNAME); table[WNAME][TNAME] = new ToStringHolder!(W, T); } static void redirect(W, T)(ref W writer, T obj) { enum WNAME = fullyQualifiedName!W; static if (!(WNAME.indexOf("__lambda") >= 0)) { // Don't register hasToString, etc registerAll!(W, moduleName!(T)); scope auto tname = typeid(obj).name; if (auto wp = WNAME in table) { if (auto p = tname in *wp) { auto tsh = cast(ToStringWriter!W) *p; assert(tsh, "Invalid ToStringWriter: "~WNAME); tsh.writeWith(writer, obj); return; } } writer.formattedWrite("<Unknown:%s>", tname); } } } // Mixin alternative instead of delegates /* enum string ToStringReal = q{ static if (!(fullyQualifiedName!W.indexOf("__lambda") >= 0)) { // hasToString static assert(__FUNCTION__.endsWith(".toString"), "Only include this mixin in toString"); if (typeid(this) !is typeof(this).classinfo) { ToStringMaster.redirect(writer, this); return; } } }; */ void realToStringOr(T,W)(T obj, ref W writer, scope void delegate() origDG = null) if (isOutputRange!(W, char)) { if (typeid(obj) !is T.classinfo) { ToStringMaster.redirect(writer, obj); return; } if (origDG !is null) origDG(); } ``` ```d class Animal { void toString(W)(ref W writer) if (isOutputRange!(W, char)) { realToStringOr(this, writer, { writer.formattedWrite("Animal()"); }); } } class Dog : Animal { void toString(W)(ref W writer) if (isOutputRange!(W, char)) { realToStringOr(this, writer, { writer.formattedWrite("Dog()"); }); } } final class Bulldog : Dog { bool sunglasses; void toString(W)(ref W writer) if (isOutputRange!(W, char)) { // We don't need realToStringOr if we know for certain this class will never be derived, but... writer.formattedWrite("Bulldog(cool: %s)", sunglasses); } } void main() { auto animals = [new Animal, new Dog, new Bulldog]; foreach (animal; animals) { animal.writeln; } } ``` ``` Animal() Dog() Bulldog(cool: false) ```
Dec 25 2022
prev sibling parent cc <cc nevernet.com> writes:
On Sunday, 25 December 2022 at 19:10:13 UTC, cc wrote:
 Fortunately, `std.format` provides a 
 non-[necessarily-]allocating alternative:

 ```d
 import std.format;
 import std.range.primitives; // Mandatory, see aside
 ```
Aside: importing `std.range.primitives` is necessary to use the templated toString version, but the compiler error for failing to do so is *highly* unintuitive as to this simple requirement: ``` C:\D\dmd2\windows\bin\..\..\src\phobos\std\format\internal\write.d(2440): Error: template instance `std.format.internal.write.formatObject!(LockingTextWriter, Animal, char)` does not match template declaration `formatObject(Writer, T, Char)(ref Writer w, ref T val, ref scope const FormatSpec!Char f)` with `Writer = LockingTextWriter, T = test.Animal, Char = char` whose parameters have the following constraints: `~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~` ` > hasToString!(T, Char) ` `~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~` Tip: not satisfied constraints are marked with `>` C:\D\dmd2\windows\bin\..\..\src\phobos\std\format\write.d(1239): Error: template instance `std.format.internal.write.formatValueImpl!(LockingTextWriter, Animal, char)` error instantiating C:\D\dmd2\windows\bin\..\..\src\phobos\std\format\write.d(632): instantiated from here: `formatValue!(LockingTextWriter, Animal, char)` C:\D\dmd2\windows\bin\..\..\src\phobos\std\stdio.d(1710): instantiated from here: `formattedWrite!(LockingTextWriter, char, Animal)` C:\D\dmd2\windows\bin\..\..\src\phobos\std\stdio.d(4227): instantiated from here: `write!(Animal, char)` ```
Dec 25 2022