Functions, overloading and default arguments: C++ and Rust

C++ lets one name carry several functions: the compiler picks one by the argument types, and a parameter may have a default so that a call can leave it out. Rust has neither feature, and the reason is not laziness. A call in Rust resolves the name first and checks the types afterwards, which keeps inference, traits and error messages tractable.

This page writes the same small task in both languages: an area of a square and of a rectangle under one name in C++, and a greeting with an optional word. Both programs are built and run below, and the last section shows what happens when the C++ shape is tried in Rust.

The task in C++

Two area functions differ in their parameter list, and greet has a default second argument. The calls below use both: one argument for the square, two for the rectangle, and one or two strings for the greeting. The output is decided by overload resolution at compile time.

The default argument belongs to the declaration, not to the function, and it is substituted at the call site. A reader of greet("Rust") has to find the header to learn that the word is "Hello".

#include <iostream> #include <string> int area(int side) { return side * side; } int area(int width, int height) { return width * height; } std::string greet(const std::string& name, const std::string& greeting = "Hello") { return greeting + ", " + name; } int main() { std::cout << area(3) << '\n'; std::cout << area(3, 4) << '\n'; std::cout << greet("Rust") << '\n'; std::cout << greet("Rust", "Hi") << '\n'; }

g++ -std=c++23 -Wall -Wextra functions.cpp -o functions
./functions

9
12
Hello, Rust
Hi, Rust

No warning and no ambiguity: the argument counts and types select the three calls without a doubt. Nothing in the output says which function ran, and that is the point of overloading.

The same task in Rust

The Rust version gives the two areas two names, area_square and area, and the two greetings become greet and greet_with. Nothing is ambiguous and nothing is hidden: the call says which function it wants, and the definition is one search away.

The shape is longer, and that length is the whole trade. The compiler never has to rank conversions to choose a function, so a call either type-checks against one signature or fails with a message that names that signature.

fn area_square(side: u32) -> u32 { side * side } fn area(width: u32, height: u32) -> u32 { width * height } fn greet(name: &str) -> String { format!("Hello, {name}") } fn greet_with(greeting: &str, name: &str) -> String { format!("{greeting}, {name}") } fn main() { println!("{}", area_square(3)); println!("{}", area(3, 4)); println!("{}", greet("Rust")); println!("{}", greet_with("Hi", "Rust")); }

rustc --edition 2024 functions.rs
./functions

9
12
Hello, Rust
Hi, Rust

The two outputs are identical line for line, which is the useful part: the programs do the same work, and the difference is only in how the names are chosen.

What differs and why

Overload resolution needs the argument types before it can pick a function, and C++ is happy to convert an argument to make a candidate fit. That is how show('a') reaches the int overload and prints 97 instead of choosing the double one or refusing the call.

#include <iostream> void show(int value) { std::cout << "int: " << value << '\n'; } void show(double value) { std::cout << "double: " << value << '\n'; } int main() { show(3); show(3.5); show('a'); }

g++ -std=c++23 -Wall -Wextra overload.cpp -o overload
./overload

int: 3
double: 3.5
int: 97

A character is an integer in C++, so the call is an exact match for show(int) and the output says int: 97. The programmer who meant a letter got a number, and the compiler had no reason to complain.

Rust resolves one name to one function and then checks the arguments against its single signature. A generic function can still work for many types, but the choice happens through trait bounds rather than through a ranked list of conversions, and a char never becomes an i32 without as.

Default arguments have a different replacement. Rust uses a second function with a full parameter list, or a builder that sets fields by name. Both put the information in the caller's code, which is where a reader looks first, and neither hides a value in a declaration in another file.

The habit that breaks

The habit is to write the two overloads as two functions with one name, which is exactly what the C++ program does. In Rust the second definition is an error, and the error is reported before any call is looked at.

fn show(value: i32) { println!("int: {value}"); } fn show(value: f64) { println!("double: {value}"); } fn main() { show(3); show(3.5); }

rustc --edition 2024 show.rs

error[E0428]: the name `show` is defined multiple times
--> show.rs:5:1
|
1 | fn show(value: i32) {
| ------------------- previous definition of the value `show` here
...
5 | fn show(value: f64) {
| ^^^^^^^^^^^^^^^^^^^ `show` redefined here
|
= note: `show` must be defined only once in the value namespace of this module

error[E0308]: mismatched types
--> show.rs:11:10
|
11 | show(3.5);
| ---- ^^^ expected `i32`, found floating-point number
| |
| arguments to this function are incorrect
|
note: function defined here
--> show.rs:1:4
|
1 | fn show(value: i32) {
| ^^^^ ----------

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0308, E0428.
For more information about an error, try `rustc --explain E0308`.

The first error is E0428: the name is defined twice in the value namespace of the module. The second is a consequence, because the only show that exists takes an i32 and the call passes a float. Fixing the first by renaming one function also fixes the second.

The C++ lesson for this ground is Functions and Overloading and default arguments; the Rust lesson is Functions and expressions, and the trait replacement is built in Traits and default methods.

The C++ programs were built with g++ 15.2.0 and the Rust programs with rustc 1.96.1, and both were run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev