Function overloading allows two or more functions to share a name as long as their parameter lists differ. The compiler picks which one to call based on the arguments at the call site. It's the feature that allows std::cout << x to work whether x is an int, a double, or a std::string, and it's how to build small, friendly APIs that read the same way for related operations.
A function name plus its parameter list together form what C++ calls the function's signature. Two functions can share the same name as long as their signatures differ in the types, number, or order of their parameters. The return type is not part of the signature. A later section returns to that point.
A small printItem family for a product catalog: one version takes a numeric product id, another takes a product name, and a third takes both an id and a quantity.
Three functions, one name, three different parameter lists. The compiler reads each call, looks at the arguments, and picks the matching version. From the caller's point of view, there's only one printItem to remember. From the implementer's point of view, there are three separate functions, each with its own body.
This is the core idea: one name, many flavors, picked by argument types. The benefit is a cleaner API. Without overloading, three different functions named printItemById, printItemByName, printItemByIdAndQty would be needed, and the reader has to remember which is which.
The signature is the function name plus the type, order, and count of its parameters. Two overloads are valid if and only if their parameter lists differ in at least one of those three ways.
Things that change the signature (so the overload is allowed):
int vs double, std::string vs int.(int, std::string) vs (std::string, int).Things that do not change the signature (so the overload is not allowed):
const qualifier on a by-value parameter.A concrete demonstration. The first two declarations are valid overloads; the third one is a compile error because it only differs from the first by parameter names.
The first two computeShipping overloads have different parameter counts, so they are different signatures. The third one has the same parameter list as the first, just renamed from weight to w. Parameter names don't enter the signature, so g++ rejects it with error: redefinition of 'double computeShipping(double)'.
Order matters too:
Both have two int parameters, but the meaning of each position is different. The types in order are (int, int) vs (int, int), which is the same signature. A better example uses parameters of different types so the order genuinely differs:
The types in order are (int, const std::string&) vs (const std::string&, int), which are different signatures. The compiler can tell them apart because the first argument's type is different in each case.
Calling a function with multiple overloads triggers overload resolution. The compiler looks at the types of the arguments, then ranks every overload by how well it matches. The best match wins. If two overloads tie, the call is ambiguous and the compiler refuses to pick.
The intuition: an exact match beats a conversion, and a smaller conversion beats a bigger one. For an int argument, an overload that takes int wins over one that takes double (which would require a conversion), which wins over one that takes const std::string& (which can't convert from int at all).
A format family for a price, with three overloads:
Walking through the four calls:
format(19.99) passes a double. The double overload is an exact match, so it wins.format(1999) passes an int. The int overload is an exact match, so it wins over the double overload (which would require an int to double conversion).format(std::string("$19.99")) passes a std::string. The const std::string& overload is an exact match.format("hello") passes a const char*. There's no const char* overload, but const char* converts to std::string, so the std::string overload wins.The decision can be pictured as a flowchart. For each call site, the compiler walks down the list of candidates and grades each one.
The diagram simplifies the real rules, which have many tiers (exact match, promotion, standard conversion, user-defined conversion, ellipsis). For day-to-day code, the simplification is enough: exact match first, then a single conversion, then ambiguity or no match. The full rules live in the C++ standard.
Overload resolution fails when two or more overloads tie for best match and neither is strictly better. The compiler refuses to guess, so it stops and reports the call as ambiguous.
An example with two overloads that each take a single numeric parameter, but the types differ:
g++ reports:
The argument 19 is an int. The compiler looks at the candidates and finds two: one takes float, one takes long. Converting int to float and converting int to long are both standard conversions of the same rank. Neither is better. The result is ambiguity, and the compiler refuses to pick arbitrarily.
Three reasonable fixes: cast the argument to the desired type, change the argument to match one overload exactly, or add an int overload to make the intent clear.
With the int overload added, price(19) becomes an exact match and the ambiguity goes away. Use the cast when the overload set isn't yours to change, and add the missing overload when it is.
Ambiguity also shows up with mixed signed/unsigned numeric types, with implicit conversions from const char* to multiple string-like types, and with default arguments interacting with overloads (covered in the section on common mistakes below).
This is a rule that causes confusion the first time. Two functions cannot be distinguished by their return type alone. With the same name and the same parameter list, even if only the return type differs, the compiler rejects the second one as a redefinition.
g++ reports:
The reason isn't arbitrary. Overload resolution happens at the call site, before the compiler knows what the caller plans to do with the return value. Consider this hypothetical call:
If the two overloads above were legal, which one would this call pick? The return value is thrown away, so neither return type gives the compiler any signal. There's no way to decide. Even when the return value is used, the compiler resolves the overload first and only then computes the result:
auto is a placeholder that the compiler fills in after the function call is resolved. The function call is resolved using the argument types, which are identical for both overloads. There's no information left to break the tie.
The same logic applies even when the return value is used in a context that expects a specific type:
The compiler doesn't look at where the result is going. It picks the function based on arguments, then checks if the return type fits the destination. Allowing return-type overloading would force the compiler to work backwards, which the language was deliberately designed not to do.
The takeaway: for two functions that do similar things and return different types, give them different names (getStockCount, getStockWeight) or different parameter types.
const interacts with overloading in a way that causes confusion the first time. The rule depends on where the const sits.
For parameters passed by reference or by pointer, const is part of the signature. So void f(int&) and void f(const int&) are two distinct overloads, and the compiler picks between them based on whether the argument is a const lvalue or a non-const lvalue.
For parameters passed by value, const is not part of the signature. So void f(int) and void f(const int) are the same function as far as the compiler is concerned, and declaring both is a redefinition.
Why the difference? With pass by value, the parameter is a local copy inside the function. Whether that copy is const or not changes how the function's body can use it, but it doesn't change what the caller sees. From the outside, both signatures take an int and copy it. There's nothing for the compiler to distinguish at the call site.
With pass by reference or pointer, the const says something about the original object: a const T& parameter can bind to a const T lvalue (an int& cannot), so the two overloads accept different sets of arguments and the compiler can tell them apart.
The full picture in code:
describe(writable) and describe(readOnly) go to different overloads because the argument is a const int lvalue in one case and a plain int lvalue in the other. The reference's const-ness is part of the signature.
Declaring both inspect(int) and inspect(const int) produces:
The compiler treats const int and int as the same parameter type when they're passed by value, so the two declarations are the same function.
A pragmatic note: when overloading on const-ness of a reference, the common pattern is const T& for read-only access and a plain T& (or T*) for mutating access. The standard library uses this pattern for member functions like std::vector::operator[], which has both a const and non-const version.
A common place where overloading helps is when one operation has several natural shapes. A shopping cart's add operation is a good example. The operation might add a single product by id, add a product with a custom quantity, or add a product by passing its name when the id isn't known yet.
From the caller's view, the API is cart.add(...). Whatever gets passed in, the cart figures it out. From the implementer's view, each overload has its own job: the (int) one defaults the quantity to 1, the (int, int) one takes both, and the (const std::string&) one does a name lookup first.
For several more shapes (add by SKU code, add by name with a quantity, add an entire pre-built line item), just add more overloads. The name add stays put.
Another natural fit for overloading is a formatting function that takes "the price" in whatever form the caller has it. Sometimes a price is a double (dollars). Sometimes it's a string already (read from a file, say). Sometimes it's a small struct that bundles cents and a currency code.
Three overloads, one purpose: turn "the price" into a printable string. The caller picks the right one by passing the right type. The output format differs across overloads because the input differs.
Overloading itself has no runtime cost. The compiler picks one of the overloads at compile time and emits a direct call. There's no virtual table, no lookup, no branching. The choice is baked into the generated code.
The third overload takes const Money& rather than Money by value. Passing a struct by const reference avoids the copy and protects the caller's object from accidental modification, which is the same general advice for any non-trivial type.
A few overloading bugs come up often enough to deserve their own callouts.
The rule appears above, but here's the mistake in its most tempting form. One function returns a parsed integer price, another returns a parsed double price. Both take the same input.
The fix is to give them different names or use a parameter to disambiguate. Different names are the cleanest path:
For one name only, pass a tag parameter:
That tag-dispatch pattern shows up in the standard library (std::piecewise_construct is one example). It's more machinery than most code needs, but it works for one name shared by related operations that return different types.
Default arguments and overloads can collide. Each overload has its own defaults, and the compiler matches calls against the full set of overloads as if every default expansion were a separate candidate. If two overloads can both match the same call, the call is ambiguous.
g++ reports:
The call addToCart(42) can hit the second overload directly, or it can hit the first overload with quantity defaulted to 1. Neither is better than the other, so the compiler refuses to pick.
Two ways to fix it. Either drop the default on the first overload (so it doesn't compete for the single-argument call), or remove the second overload (so the default fully owns the single-argument case).
Option B is usually the right choice. The point here is to flag the interaction so it doesn't get hit by accident.
When two parameter types are implicitly convertible to each other, calls with a third type often go to the "wrong" overload. The fix is usually to add the missing overload directly.
This prints bool: 1. The string literal "on sale" is a const char*, which converts to bool (any non-null pointer is true) and to std::string. The pointer-to-bool conversion is a standard conversion; the pointer-to-std::string conversion is a user-defined conversion. Standard conversions beat user-defined conversions, so the bool overload wins.
The fix is to add a const char* overload (or disable the bool one by making it take something stricter):
Now show("on sale") is an exact match for the const char* overload, which forwards to the string version. The unexpected behaviour is gone.
Overloading is a tool, not an obligation. Two situations where it's the wrong call.
First, when the two functions do different things. Overloading is for the same operation on different argument shapes. A function that adds an item to a cart and another that removes one shouldn't share a name even if their parameter lists differ. The name should describe what the function does.
Second, when the parameter types are very similar and prone to confusion. With charge(double dollars) and charge(int cents), a caller writing charge(19) might mean nineteen dollars or nineteen cents. The compiler will pick one (the int overload, in this case), but the caller may have meant the other. Distinct names like chargeDollars and chargeCents remove the trap.
Overloading helps when the operation is genuinely the same and the type variation is a convenience for the caller. When in doubt, ask whether std::cout << x (overloaded for every printable type) would be a good model for the API. If yes, overload. If not, use distinct names.
10 quizzes