How method signatures are determined

How method signatures are determined

23.Sep.2021

The first step of processing a method call is to figure out what it does and how it will be processed. That isn't as simple as walking C++ code and looking for function calls; there are lots of ways you can write functions that end up doing the same thing (think about all the times you wrote something like `void do_it(int x, int y)`. Also keep in mind things like constructors).

 

What Determines How the Method Will Be Processed?

The compiler looks at two main things to determine how a method should be processed: The return type and the argument types . By determining those two things, it can then deduce if the method returns `this`, takes/returns pointers, references, and arrays, etc.

 

The return type tells the compiler how to handle the end of a method; this is also where you can get into some situations that aren't handled by default (like returning void from a non-void function...). The argument types tell the compiler what needs to be passed in and how it needs to be handled.

 

How does the compiler know what to do with these two things? By using something called overload resolution . This means trying all possible combinations of functions until one matches or there are no more possibilities for matching methods. This process takes both type information - so if there are multiple methods which could work given their return/argument types - AND other contextual information about the code being compiled. This contextual part plays a big part in why some things won't work and I'll talk about it more below.

 

Why does Overload Resolution Matter?

The short answer is, "because you need to know this or your code won't work". The longer answer is because D tries to make overloading as painless as possible, but there are still lots of situations where the compiler has to do extra work that causes unexpected behavior. It's good to understand why these things happen so you can avoid them or figure out how to get around them (perhaps with static if blocks ;)). There are also cases where this knowledge will help you determine whether something will compile (and at times even help you find bugs).

For example: How does the compiler know how to process this?

void foo(int x)

 

If you think about it, there are two possible scenarios for this code. The first is where the compiler knows that because `foo` takes an int parameter that it should just pass them both on to the integer `x`. The second scenario is where the compiler has no idea how to handle passing two arguments to an int (since by default value types cannot be passed by reference). So what does the compiler do in this case? Well, it must use overload resolution to determine which method should be used. There are actually other options here too, but I'll go over those later since they aren't relevant right now. But let's say we have two functions defined like so:

 

void foo(int x)

int bar(ref int y)

 

Now the compiler has to choose between these two functions. It starts by looking at both return types since they are required to be matched in order for a function to be considered as an option. Since it sees that `foo` returns void and `bar` returns an int, it can now match up those types with the arguments given. So if you called `foo(1, 2)`, then the compiler would know that it should pass one argument of type int (which is the first argument), and another argument of type int (which I'll call 2). Then it will pass all this information onto integer parameter X which will have received what was passed into it. This means `foo(1, 2)` would call `bar`.

Now if you passed that into `void foo(double d, double e)`, then it would know that because `foo` takes two doubles (or floating point numbers - same thing) that the first argument should be passed as a double and the second argument should also be passed as a double. The compiler can do this because both parameters of type double are required to be matched. This is why when you send in more than one value (like strings or arrays), all those types need to match up exactly; otherwise the compiler wouldn't know what to do with them.

So say we called `void foo(int x, int y)` instead of `void foo(int x)`. Then the compiler would choose between the two functions by matching up return types and arguments in a similar way to before (and in this case it would pick `foo`). The difference here is that when using `foo`, any function with an integer parameter and another integer parameter where at least one of them returns void will match. So these too:

 

void foo(int x, int y)

void bar(int z, int w)

void baz(ref int r, ref int p)

would all be valid options if called with `foo((1, 2), 3)`. This is because you're allowed to pass in more than just two integers. However if we wanted to call `void foo(int x, int y)` with just one integer as an argument (like `foo(1)`, we would get a compiler error because there's no remaining option that can be used.

So if we wanted to call this function and we had only one integer available (or the same amount of integers), what do you think the compiler would do? That's right: It wouldn't know which method to use and it would give you a "method not found" compiler error.

There isn't really anything to worry about here; by design D requires that any parameters be matched up exactly for functions to match (with the exception of ref/out). This means for any function that has overlapping parameters, all the types must be the same. For example `void foo(int x)` and `void foo(long l)` would not match in a call to `foo(1)`. This is because although both functions have a long parameter, they also both require that argument to be of type int (and it needs to match up with what was passed).

Now there are times when you will want some flexibility like this. And for that, D has two special features: Default parameters and mixins. I'll talk about mixing in more detail later; since it's not directly related to function overloading (although it does serve a similar purpose), but default parameters allow you to set default values for any method you define (or even all the ones in a particular module).

You can use this for your benefit to make your code more flexible and that much easier to call. It also saves you from having to write (and rewrite) many functions with similar parameters. For example:

   void foo(int x = 3, int y = 4) {      // Do something... }

Then if we called `foo()`, it would work just like any other function with two arguments because the compiler will add whatever it needs so that the calling arguments match up exactly with what was defined in the function signature. So `foo()` would be equivalent to `foo(1, 2)`. And when we called `foo(1); foo();`, then it would be like `foo(1, 2); foo(3, 4)` and the compiler would add the second parameter (making it 3 and 4 respectively).

So just by adding two numbers together, we were able to reduce our argument count down from three to two with no changes needed to how we're calling it! We just got a little bonus flexibility. This is beneficial in that you don't have to write/rewrite functions all the time or check types if they aren't even necessary; but what's also nice is that we don't have any unneeded parameters either. It's safe and simple.

We are social