D's Default Initializers

posted in D Bits
Published July 03, 2011
Advertisement
I've been away from Dolce for a couple of weeks now. Just came back to it last night and realized I don't like it. I've horribly over engineered some of the modules. So from last night I started stripping stuff out and refactoring. In the process, I realized a silly mistake in my resource management code. I'm throwing it out and rewriting it anyway, but it inspired a topic for this blog.

My resource system was, overall, designed to work with any imaginable resource. So it's template-based and has a flexible interface. But I started from the perspective of Allegro resources, which means working with struct pointers. And I imagined that other potential resources that I might want to use would be class based. That led to an implementation detail that could cause compilation to fail in certain cases.

The problem boils down to something like this. Given a member called _resource of type T, I want to clear it out when I no longer need it in certain circumstances. Since I'm dealing with struct pointers and classes, which are always references, I can just do this:


_resource = null;


That works and does what I want. Then to determine whether a resource is loaded I can just test _resource for null. Until, of course, I decide one day to do something like use a struct resource by value, rather than as a pointer. Not something too farfetched. In that case, I'd get a compiler error. While DMD's template error messages are a good deal better than what most C++ compilers give us, it's annoying to get them. And there's no reason why I shouldn't support non-nullable types.

So here's a contrived example of what happens in this situation.


module nullify;

void nullify(T)(T t)
{
t = null;
}

void main()
{
int i = 10;
nullify(i);
}


So nullify is a templated function with no constraints, meaning it can accept any type at all (I'll talk a bit about D's template syntax another day -- this is a specific case where you can declare the template without the template keyword and call it as you would a normal function). Try to compile this code and you'll get the following output:


nullify.d(8): Error: cannot implicitly convert expression (null) of type void* to int
nullify.d(14): Error: template instance nullify.nullify!(int) error instantiating


Right. To solve this problem, there are two obvious choices. One is to use template constraints to restrict the template only to pointers and classes. There would still be compiler errors of a different nature, but it would be a signal that this template is not intended to work with value types. In some cases, that might be preferable. In this particular case, a better option is to make use of default initializers.

Every type in D has a default value to which instances are automatically initialized on declaration. For example, ints are initialized to 0, floats to nan, classes and pointers to null. This value is readable as a property, .init, both on the type and on the instance. So we can modify the nullify template above like so:


void nullify(T)(T t)
{
// You could use t.init or T.init here.
t = T.init;
}


Now the code will compile. Pointers and classes will be set to null, floats and doubles to nan, characters to 0xff, and so on. What about value structs? Try this:


module nullify;

import std.stdio : writeln;
import std.string : format;

void nullify(T)(T t)
{
t = T.init;
writeln(t);
}

void main()
{
struct Foo
{
int x = 10;
int y;

// Without a toString method, the name of the type
// would be output by default in the call to writeln
// above. In this case, "Foo".
string toString()
{
return format("(%d, %d)", x, y);
}
}
Foo f = Foo(23, 38);
nullify(f);
}


The output from this is "(10, 0)". Foo.y, as an int, has the default initializer 0. I've changed the default initializer of Foo.x, however, in the definition of Foo. So all instances of Foo will have the value 10 for x on instantiation. This is not the same as assignment. For example, this will not print 10, but 0:


int i = 10;
nullify(i);


Here, we are declaring an instance and assigning a value to this instance. We are not defining a type. Big difference.

So using default initializers is a convenient way to clear out a templated class/struct member for any given type. Then, tests like a hypothetical isLoaded method become


bool isLoaded()
{
return _resource == T.init;
}


You can read more about the .init property in the D documentation at d-programming-language.org.
0 likes 2 comments

Comments

capricorn
Ahem.
[code]
int i = 10;
nullify(i);
[/code]

This will print 0 indeed, but this:

[code]
int i = 10;
nullify(i);
assert(i == int.init);
[/code]

will print 0 and then throw an assertion, at least with the signature of nullify that you present here. Reason being that the parameter is passed by value [img]http://public.gamedev.net/public/style_emoticons/default/smile.gif[/img]
July 24, 2011 05:11 PM
Aldacron
Yes, of course. The template to modify by ref:

[code]
void nullify(T)(ref T t)
{
t = t.init;
}
[/code]

Probably would have been better for me to use this form for the example.
July 31, 2011 12:04 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement