What is the best way to program over "abstract" types in D?

Adam D. Ruppe destructionator at gmail.com
Sat Nov 23 15:06:43 UTC 2019


On Saturday, 23 November 2019 at 11:21:32 UTC, Ola Fosheim 
Grøstad wrote:
> However, virtual functions can be slow.
> [...]
> (so you don't have to require Heap allocation with classes)?

Is your worry in slowness of virtual functions or heap allocation?

Neither is necessarily a problem with classes. The compiler can 
optimize out a lot of virtual calls, and heap allocation is 
pretty easily avoided in your own code (and in theory the 
compiler could optimize that too, but it doesn't as far as I know 
right now).

Take a look at this code:

---

class Base {
         void doLotsOfStuff() {
                 import std.stdio;
                 writeln("I am doing lots of stuff.");
                 writeln(childResult());
                 writeln("cool");
         }

         int defaultValue() { return 5; }

         abstract int childResult();
}

final class Child : Base {
         override int childResult() { return defaultValue() + 5; }
}

void workOnIt(T : Base)(T obj) {
         obj.doLotsOfStuff();
}

void main() {
         import std.typecons;
         auto c = scoped!Child; // on-stack store
         Child obj = c; // get the reference back out for template 
function calls

         workOnIt(obj);
}

---


The three lines in main around scoped make it a stack class 
instead of a heap one.

The `final` keyword on the Child class tells the compiler it is 
allowed to aggressively devirtualize interior calls from it (in 
fact, ldc optimizes that `childResult` function into plain 
`return 10;`) and inline exterior calls to it.

`workOnIt` can similarly optimized automatically in many cases, 
even a a plain function if it gets inlined, but here I showed a 
template. If the optimizer fails on the plain function, trying a 
template like this can allow that `final` to propagate even 
further. In my test, it simply inlined the call.

The only function here that didn't get devirtualized is the 
abstract method itself, childResult. All the surrounding stuff 
was though.


If you want to get even that devirtualized.... there's the 
curiously-recurring template pattern to make it happen.

----
// base is now a template too
class Base(T) {
	void doLotsOfStuff() {
		import core.stdc.stdio; // I switched to printf cuz cleaner asm
		printf("I am doing lots of stuff.\n");

                 // and this cast enables static dispatch...
		printf("%d\n", (cast(T) this).childResult());
		printf("cool\n");
	}

	int defaultValue() { return 5; }

	abstract int childResult();
}

// child inherits base specialized on itself, so it can see final
// down in the base as well as on the outside
final class Child : Base!(Child) {
  	override int childResult() { return defaultValue() + 5; }
}

void workOnIt(T : Base!T)(T obj) {
	obj.doLotsOfStuff();
}

void main() {
	import std.typecons;
	auto c = scoped!Child; // on-stack store
	Child obj = c; // get the reference back out

	workOnIt(obj);
}
----


It depends just how crazy you want to go with it, but I think 
this is less crazy than trying to redo it all with mixins and 
structs - classes and interfaces do a lot of nice stuff in D. You 
get attribute inheritance, static checks, and familiar looking 
code to traditional OO programmers.

BTW the curiously-recurring template pattern is fun because you 
also get static reflection through it! I wrote a little about 
this in my blog a few months ago: 
http://dpldocs.info/this-week-in-d/Blog.Posted_2019_06_10.html


If you really wanna talk about structs specifically, I can think 
about that too, just I wouldn't write off classes so easily! I 
legit think they are one of the most underrated D features in 
this community.


More information about the Digitalmars-d-learn mailing list