I'm trying to be defensive on one front and am over-defending another. What you are asking me for is type safety, and yes, we can get that. What you're also asking me for in the process is to not use an integer, and that would be fallacy. I stand by my original statement—integers as our reference are opaque. The fact that you have reference collisions does not make the data any less opaque. The point of integers is that they're dense and easy to track—it's trivial to check if a sprite is loaded given its index, and it's harmless to be wrong about whether it's still loaded.
Yes, collisions happen all the fucking time, and when concepts are genuinely confusing—eg, I asked you for a texture index, you passed me a sprite index or a background index—head-scratching behavior can occur.
We don't have the framing right now to really lick this. How about this: In the interim, why don't you replace
int in these functions with
sprite_t,
background_t,
path_t, etc. For now, please just use [snip=edl]typedef int sprite_t;[/snip], etc.
Here's where we run into trouble: you are also asking me for overloads. There's a lot more involved in overloading [snip=edl]widget_set_parent(button_t button, window_t window)[/snip] with [snip=edl]widget_set_parent(window_t window1, window_t window2)[/snip] than you are giving credit for. The compiler
can generate an array for the cross-product of all these overloads, if it also generates RTTI metadata for them, but then you still have new problems.
Your first problem is going to be that C++ won't let you overload these integer types. Since they're all integers, you can't create this overload. You can get around this by using a struct instead of a typedef. In doing this, you have created the problem that you can't store these in integers at all, anymore, which may be what you want. If you do this, I recommend having these all inherit a class called
reference_index which just stores an integer.
This will allow you to roughly implement this alongside the current variant implementation, with a little tweaking.
The second problem is arguably worse. All this sounds great until you realize that you have just moved the problem of overload resolution
to runtime.
So let's assume you've overloaded
widget_set_parent for a number of different types. You'll have something like this:
struct window_t: reference_index { /* ... */ }
struct button_t: reference_index { /* ... */ }
void widget_set_parent(window_t window, window_t parent);
void widget_set_parent(button_t button, window_t parent);
Naively, you can give the user methods to "cast" a variant between those. To involve the compiler is to introduce our run-time compile errors. We tell the compiler that it's okay for a user to pass variant to these functions. To enable this to happen, the compiler will generate a run-time type enumeration for variant, like so:
namespace enigma {
enum variant_runtime_types {
VRT_SPRITE,
VRT_BACKGROUND,
// ...
VRT_WINDOW,
VRT_BUTTON,
VRT_COMBOBOX,
// ...
}
}
It can do this, for example, by querying for members structs of
enigma_user which extend
reference_index. No problem. It will also generate special methods to fetch these, as needed, and we'll assume
- that this is done in a way that does not involve modifying var.h when this list changes, so
- nothing in the engine code relies on that constructor, and so the logic does not need to be known at compile time; and that
- the actual constructor logic (or most of it) is implemented in the engine's main source where all other user code is generated
I have pasted
a sample execution of the idea to Pastebin. It shows the basic stages of this, but does not show a complete variant, nor the cast method. But the cast method looks very similar to the construct method, only it actually checks the value of
variant.rtti before returning. In practice, we'll probably replace the template function I showed there with a structure containing those so that we don't generate linker errors in problem scenarios (the missing information will be caught at compile time).
This is fine and dandy, but the C++ compiler
can't tell which type a variant should use, because a variant can cast to any of those. Thus, all of those methods are weighted equally to the overload resolving compiler.
We have lost compile-time overload checking, which is pretty much nothing new, I suppose. But now the compiler has to deal with this to allow it to happen at all.
To do this, the compiler must first identify functions whose overloads are ambiguous to
variant in ISO C++. This is a painful check, but it's doable. The compiler must then generate overloads taking
const variant&/
const var& for these types. This requires two pieces.
First, we need to declare a place for runtime overload disambiguators:
// Runtime overload disambiguation
map<tuple<int>, void(*)(variant, variant)> widget_set_parent$_overloads;
static void widget_set_parent$button$window(variant arg0, variant arg1) {
widget_set_parent((button_t)arg0, (window_t)arg1);
}
static void widget_set_parent$window$window(variant arg0, variant arg1) {
widget_set_parent((window_t)arg0, (window_t)arg1);
}
static inline void widget_set_parent$_disambiguate(const variant& arg0, const variant& arg1) {
map<tuple<int>, void(*)(variant, variant)>::iterator it = widget_set_parent$_overloads.find(tuple<int>(arg0.rtti, arg1.rtti));
if (it != widget_set_parent$_overloads.end())
return it->second(arg0, arg1);
show_error("No overload for widget_set_parent(" + rtti_names[arg0.rtti] + ", " + rtti_names[arg1.rtti] + ")");
return void();
}
The above code assumes we also export the names of these types into an array somewhere, which is also dastardly ugly. It also assumes the existence of a tuple class which is basically a vector that I can construct really easily to save code.
Then, we need to populate that map at load time:
void load_overload_disambiguators() {
widget_set_parent$_overloads[tuple<int>(VRT_BUTTON, VRT_WINDOW)] = widget_set_parent$button$window;
widget_set_parent$_overloads[tuple<int>(VRT_WINDOW, VRT_WINDOW)] = widget_set_parent$window$window;
}
And if you want implicit casting, well, you're looking at even more logic generated for the overload disambiguation routine. Coupled with even more metaprogramming-fueled metadata.
But anyway, now you have a very thorough synopsis of how I'd handle it. I imagine if I don't get around to it now, I'll get around to it after you all sit on it for three years and I've long forgotten it's a problem.