In this article, we’ll learn about function overloading ποΈππ·ββοΈπ»π, a useful and interesting feature in the Solidity programming language.
It’s part of our long-standing tradition to make this (and other) articles a faithful companion or a supplement to the official Solidity documentation. We’ll base this article on the original Solidity programming language content.
Overview
Function overloading is a feature that enables us to define more than one function with the same name but with different combinations of function parameters in terms of their number and type.
Roughly speaking, we’d usually go with the function overloading approach when we need:
- Different variants of the same base functionality or business logic, i.e. a function performing the addition of integers vs floating-point numbers;
- Specialized versions of one generalized base functionality, i.e., a function adding two integers instead of ten integers. In this case, a specialized version of the base functionality can be considered a function wrapper because it contains a call to the base function with some parameters commonly hardcoded to default values.
π‘ Note: In a sense, overloading can also be applied to inherited functions, but in that case, we refer to it as function overriding. When a function overrides an inherited function, it has the same signature as the overridden function, i.e., the same function name, parameter number, and parameter type. Effectively, the overriding function redefines the inherited (base) function behavior, but only on a sub-class level.
With that said, let’s just remember that function overloading produces more variants of the same-name function, while function overriding just redefines an inherited function.
Function overloading cannot be achieved by renaming the function parameters, i.e., function parameter names don’t affect the function signature and cannot produce an overloaded function.
Example – Function Overloading
The following example shows how function overloading works. As we always do, we’ll analyze it segment by segment.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0;
A regular contract defines two functions with the same name, but with a different number of parameters and parameter types.
contract A {
The first function f takes only one parameter, value, of type uint. The function behavior is extremely simple: it just returns the received parameter value by assigning it to the output parameter out.
function f(uint value) public pure returns (uint out) { out = value; }
The second function f
takes one more parameter, really, of type bool
, but also keeps the same parameter from the first function f
definition, value.
The function behavior is just a shade more complex: it conditionally returns the received parameter value by assigning it to the output parameter out, i.e., if the parameter really is set to true.
Otherwise, the parameter out returns the default value, determined from its variable type uint
, which is 0.
function f(uint value, bool really) public pure returns (uint out) { if (really) out = value; } }
Example – Function Overloading in External Interface
Before we analyze another example of function overloading specific to external interfaces, let’s shortly discuss what’s so special about external interfaces.
Functions in external interfaces introduce two kinds of types: Solidity types and external (ABI) types. For instance, Solidity can have multiple address types, such as contract
and address
. However, from the ABI perspective, the mentioned types are regarded as the same type, which would cause an error during the compilation if used in an overloading attempt, as in our example:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0;
A very simple example showing an attempted function overloading with different Solidity types, but the same (after compilation) ABI types.
contract A {
Input and output type B
is contract type from the Solidity perspective, but it’s also address type from an ABI perspective.
function f(B value) public pure returns (B out) { out = value; }
Input and output parameter types address are the same from the Solidity perspective and from the ABI perspective in both overloaded functions.
function f(address value) public pure returns (address out) { out = value; } }
Declares the placeholder contract B
for parameter typing purposes.
contract B { }
After compilation, both functions are declared as public (public = external + internal), take the same overloaded form and therefore cause an error, since they both have input and output parameter types declared as address
, hence causing improper overloading from ABI (compiler output) perspective:
β‘ TypeError: Function overload clash during conversion to external types for arguments.
Implementation
Now that we know what function overloading is, let’s see how it works. π
A function call to an overloaded function is resolved by the function name and argument matching, meaning the right function in the current scope is selected depending on its name and the arguments in the function call.
Specifically, overloaded functions are selected as candidates if their arguments are implicitly convertible to the expected parameter types. Of course, if the function parameter conversion fails for only one parameter, the resolution fails and the function candidate is entirely dismissed.
π‘ Note: Function return parameters (types) are not part of the function resolution process.
To make the story of implicit conversion more clear, we’ll look at the example below:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; contract A { function f(uint8 val) public pure returns (uint8 out) { out = val; } function f(uint256 val) public pure returns (uint256 out) { out = val; } }
In this example, we have a function f
overloaded in two instances: the first instance takes and returns a variable of uint8
type, while the other instance takes and returns a variable of uint256
type.
A call f(50)
would return a type error because it’s ambiguous, i.e., it cannot precisely determine the right function instance since the argument 50 can be implicitly converted to match both functions. However, when we make a call f(256)
, a function f(uint256)
is the only one that matches the result of the implicit conversion of argument 256, so this call proceeds without any errors.
Conclusion
This article taught us about function overloading and compared it with function overriding.
First, we made an overview to understand what we’re talking about.
Second, we went through an example showing how function overloading looks in its simplest form.
Third, we studied a bit more complex example of function overloading with regard to external interfaces.
Fourth, we went into just a bit more detail, showing how the overloaded function resolution works under the hood.
What’s Next?
This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):