Should I Use Overloads or Default Parameters?
“Should I use overloads or default parameters”, haven’t you asked yourself that question?
When designing an interface where the user can leave the value of an argument up to the API, two approaches are possible:
Using a default parameters:
void drawPoint(int x, int y, Color color = Color::Black);
And using overloading:
void drawPoint(int x, int y); // draws a point in black void drawPoint(int x, int y, Color color);
Which approach is cleaner? Which expresses better the intentions of the interface? Or is it just a matter of style?
This may be subjective, but I’m under the impression that overloading tends to have better popularity than default parameters amongst C++ developers. But I believe that both features have their usages, and it’s usefulto see what makes one or the other more adapted to a given situation.
This post is part of the series on default parameters:
- Default parameters in C++: the facts (including the secret ones)
- Should I overload or use default parameters?
- Default Parameters With Default Template Type Parameters
Defaulted
: a helper to work around default parameters constraints- Implementing Default Parameters That Depend on Other Parameters in C++
- How default parameters can help integrate mocks
Default parameters: a loud and clear message to the API users
By default, I think that we should prefer default parameters rather than overloads.
Indeed, default parameters send a very clear message to the client of the API: whether or not you’re passing a value for the default parameter, it’s the same code that will be executed.
Indeed, whether you call:
drawPoint(10, 12, Color::Black);
or just
drawPoint(10, 12);
you’re 100% sure that you will get into the same code. Indeed, there is just one function!
On the contrary, overloaded functions go in groups, by definition. So calling
drawPoint(10, 12);
calls the first overload, while:
drawPoint(10, 12, Color::Black);
calls the second overload, which is a different function.
True, in such a case you’re expecting to reach the same code eventually, otherwise the interface would be very surprising. But aren’t there surprising interfaces out there? Or can’t a bug sneak in between the two overloads, making them behave slightly differently? It can.
What’s more, default parameters express that there is really one function to which you don’t have to provide all the inputs. This is really about the intent of the interface.
The default value
The above example is an obvious one, but using default parameters has the advantage of being explicit about the default value. Indeed, overloads don’t tell what would be the value used if the API client does not provide it. You can see that the example resorted to comments to communicate about this value:
void drawPoint(int x, int y); // draws a point in black void drawPoint(int x, int y, Color color);
And whether comments are a good thing or not, this one is more fragile than a value hard-coded in the interface, because if the first overload stops using Color::Black
there is a risk of the comment being left as it is and becoming misleading.
So default parmeters is the choice I would recommend by default. But there are some cases where overloads make more sense: delegating constructors, groups of arguments, and APIs that don’t get compiled at the same time as their client.
Delegating constructors
C++11 introduced delegating constructors, which can achieve similar things as default parameters:
class Rectangle { public: Rectangle(int width, int height) : Rectangle(width, height, Color::Black) {} Rectangle(int width, int height, Color color); // ... };
Their usage look like this:
Rectangle r1(10, 12, Color::Black); // calls 2nd ctor Rectangle r2(10, 12); // calls 1st ctor, that falls right into 2nd ctor
(Note that this interface would be a good case for using strong types, but let’s focus on default parameters versus overload here.)
Here, even though there are two constructors, we are 100% guaranteed that they both fall into the same code (the one of the second constructor). This happens by definition of delegating constructors, and not because we trust an API to do what we expect. And the default value is also explicit in the interface.
Note though that this is because the implementation of the above delegating constructor is present in the class declaration. If it were in a separated cpp file, it would be equivalent to an overload from the point of view of the interface:
class Rectangle { public: Rectangle(int width, int height); // does this fall into the 2nd ctor? // what is the default value? Rectangle(int width, int height, Color color); // ... }; // Somewhere else, in a .cpp file...: Rectangle::Rectangle(int width, int height) : Rectangle(width, height, Color::Black){}
In this case, the delegating constructor only serves implementation purposes, to factor code (even though this is valuable too!).
Default parameters don’t work in groups
One of the features of default parameters is that we can’t have one default value for several arguments at the same time. Let’s leave the color argument aside for a moment, and say that we want a default location for the point to drawn by our interface: 0,0.
If we used default parameters , the interface would look like this:
void drawPoint(int x = 0, int y = 0);
But this would allow the following code to compile:
drawPoint(10);
And it isn’t what we wanted. Our requirement was a default location, not a default y-coordinate. And we can’t achieve this directly with default parameters because they don’t work in groups.
At this point you have two options. The first one is to create a Point
structure:
struct Point { Point(int x, int y) : x(x), y(y) {} int x; int y; };
And have a default value for that argument:
void drawPoint(Point const& p = Point(0,0));
If the two arguments are related, like x
and y
are here, it makes sense to create such a structure. But it’s not always the case. And when it’s not then it makes sense to use overloading:
void drawPoint(); // draw a point at the origin void drawPoint(int x, int y);
This ensures that we don’t have the default value kicking in for half the arguments only.
Default parameters are baked in client’s code
One last aspect of default parameters that may sound surprising at first is that the resolution of the default parameter is made
- at compile-time,
- and at call site.
What this means is that, going back to our initial example, when you write this:
drawPoint(10, 12);
the compiler compiles a code equivalent to that:
drawPoint(10, 12, Color::Black);
In particular, the resolution of the default parameters is not done at the beginning of the function. It is done at the call site.
This can have consequences. One of the practical consequences of this is that if you change the default value in the API to, say, Color::Red
instead of Color::Black
:
void drawPoint(int x, int y, Color color = Color::Red);
The compiled call site will still look like this until it is re-compiled:
drawPoint(10, 12, Color::Black);
So even if we change the API and set it a new default value, the call site keeps the same behaviour by using the old default value, until it is recompiled. This can lead to surprising results and hard-to-find bugs.
Overloads, by hiding the default value inside of the implementation, don’t have this problem.
But should you care? This depends on how public your API is. If we’re talking about an internal API that get compiled along with its clients with the standard build of your application, then this nuance doesn’t matter much.
But if your API is public and used by other applications, or by clients outside of your company then you should care, and prefer overloads to avoid unexpected behaviour until the clients recompile their code after an upgrade of your API.
In summary, to choose between overloads and default parameters, I would recommend default parameters in the general case. But there are some situations where overloads make more sense: delegating constructors, groups of arguments, and APIs that don’t get compiled at the same time as their client
I hope this will help you make decisions when you choose between default parameters and overloading. Your comments are welcome.
You may also like
- Default parameters in C++: the facts (including the secret ones)
- Default Parameters With Default Template Type Parameters
Defaulted
: a helper to work around default parameters constraints- Implementing Default Parameters That Depend on Other Parameters in C++
- How default parameters can help integrate mocks
Share this post!