Operator overloading: C++ and Rust
Both languages let a type define what an operator means. C++ writes a free function or a member named operator+ and a stream operator operator<<; Rust implements the traits Add and Display. The difference is the conversion: C++ may build a temporary through a constructor to make a call fit, and Rust never converts an operand.
The task in C++
The money class has a constructor from cents, an operator+ that adds two values and an operator<< that writes one to a stream. The program adds two money values and prints the result in cents.
The output is three hundred and fifty cents, and the stream operator is what makes std::cout << total work. Both operators are ordinary functions with a special name.
#include <iostream>
class Money
{
public:
explicit Money(int cents) : cents_(cents) {}
Money operator+(const Money& other) const { return Money(cents_ + other.cents_); }
int cents() const { return cents_; }
private:
int cents_;
};
std::ostream& operator<<(std::ostream& out, const Money& money)
{
return out << money.cents() << " cents";
}
int main()
{
Money price(250);
Money total = price + Money(50);
std::cout << total << '\n';
Money with_five = price + Money(5);
std::cout << with_five << '\n';
}
g++ -std=c++23 -Wall -Wextra operators.cpp -o operators
./operators
300 cents
255 cents
The second line of the output comes from adding a second value of five cents. The implementation takes the exact type, and nothing here converts anything.
The same task in Rust
The struct implements Add with Output = Money and Display with the formatting method. The addition uses the trait, and the print uses the braces that call the trait.
The program prints three hundred cents. There is no stream operator: printing goes through the format machinery, and the type decides how it appears in any format string.
use std::fmt;
use std::ops::Add;
struct Money {
cents: i32,
}
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money { cents: self.cents + other.cents }
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} cents", self.cents)
}
}
fn main() {
let price = Money { cents: 250 };
let total = price + Money { cents: 50 };
println!("{total}");
}
rustc --edition 2024 operators.rs
./operators
300 cents
The trait method takes self by value, so the left operand is moved. The lesson on operator traits covers the alternatives and the Index trait.
What differs and why
Who owns the value: the C++ operator returns a new object built from the two operands, and the Rust version does the same through the Output type. Neither mutates the operands, and both may take them by value or by reference depending on the signature.
What the compiler rejects: C++ overload resolution considers conversions, so an integer can become money through a non-explicit constructor. Rust has no implicit conversion, so the right operand must be the type named in Add, or the call is E0308.
What happens at run time: the same addition either way. The difference is that the Rust call site knows exactly which implementation runs, because the types on both sides of the operator are part of the signature.
The habit that breaks
The habit is price + 50. In C++ the constructor turns fifty into money and the operator runs, which compiles and prints the sum. The temporary is invisible at the call site.
#include <iostream>
class Money
{
public:
Money(int cents) : cents_(cents) {}
Money operator+(const Money& other) const { return Money(cents_ + other.cents_); }
int cents() const { return cents_; }
private:
int cents_;
};
int main()
{
Money price(250);
Money total = price + 50;
std::cout << total.cents() << '\n';
}
g++ -std=c++23 -Wall -Wextra operators_wrong.cpp -o operators_wrong
./operators_wrong
300
The Rust version of the same line is a type error. To accept an integer, the type needs a second implementation, impl Add<i32> for Money, and then the conversion is written in the type rather than in a constructor.
use std::ops::Add;
struct Money {
cents: i32,
}
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money { cents: self.cents + other.cents }
}
}
fn main() {
let price = Money { cents: 250 };
let total = price + 50;
println!("{}", total.cents);
}
rustc --edition 2024 operators_wrong.rs
error[E0308]: mismatched types
--> operators_wrong.rs:17:25
|
17 | let total = price + 50;
| ----- ^^ expected `Money`, found integer
| |
| expected because this is `Money`
|
help: try wrapping the expression in `Money`
|
17 | let total = price + Money { cents: 50 };
| ++++++++++++++ +
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
The C++ lesson for this ground is Operator overloading, and the Rust lesson is Operator traits. The next page in this set is Macros and templates.
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