Templates and generics: C++ and Rust
A C++ template and a Rust generic both write one function for many types, and both produce a specialised version for every type they are used with. The difference is what the definition promises. A template promises nothing and is checked when it is instantiated; a generic function states its requirements as bounds and is checked when it is written.
The task in C++
The template twice returns its argument added to itself. It works for an integer, a double and a std::string, because all three have +. The compiler writes a version for each call.
The program prints forty-two, three and abab. Nothing in the template says which types are allowed, and the errors for a bad type appear only when such a call exists.
#include <iostream>
#include <string>
template <typename T>
T twice(T value)
{
return value + value;
}
int main()
{
std::cout << twice(21) << '\n';
std::cout << twice(std::string("ab")) << '\n';
std::cout << twice(1.5) << '\n';
}
g++ -std=c++23 -Wall -Wextra templates.cpp -o templates
./templates
42
abab
3
The string version returns a new string, and the two additions are visible in the output. The template body is the same three tokens in all three versions.
The same task in Rust
The generic function declares T: Add<Output = T> + Copy, which promises the addition and the copy that the body needs. The bound is the whole difference: the body compiles because of it, and every call has to satisfy it.
The program calls it with an integer and a float. The string version is missing on purpose: the standard String does not implement Add in the same way, which the lesson on operator traits explains.
use std::ops::Add;
fn twice<T: Add<Output = T> + Copy>(value: T) -> T {
value + value
}
fn main() {
println!("{}", twice(21));
println!("{}", twice(1.5));
}
rustc --edition 2024 templates.rs
./templates
42
3
The two calls produce the two numbers. The bound is checked at the call site, and the error for a type without the trait names the trait rather than a line inside the body.
What differs and why
Who owns the value: both versions take the argument by value and return a new one, so both copy or move. The Rust version requires Copy because the body adds the value twice, and that requirement is written down; the C++ version copies implicitly and never says so.
What the compiler rejects: C++ accepts the template definition for any type and reports the missing + only at the instantiation, inside the body. Rust rejects a body that uses an operation the bounds do not allow, and the error points at the operation in the definition.
What happens at run time: nothing different. Both compilers generate a specialised function per type, and both calls are direct. The trade is code size, which monomorphisation pays in both languages.
The habit that breaks
The habit is to write a generic that only works for some types and let the instantiation find out. The C++ template below compiles while it is only a definition; the error appears when a struct without + is passed, and it points inside the template.
#include <iostream>
struct Counter
{
int value;
};
template <typename T>
T twice(T value)
{
return value + value;
}
int main()
{
Counter counter{1};
std::cout << twice(counter).value << '\n';
}
g++ -std=c++23 -Wall -Wextra templates_wrong.cpp -o templates_wrong
echo "status: $?"
templates_wrong.cpp: In instantiation of 'T twice(T) [with T = Counter]':
templates_wrong.cpp:17:23: required from here
17 | std::cout << twice(counter).value << '\n';
| ~~~~~^~~~~~~~~
templates_wrong.cpp:11:18: error: no match for 'operator+' (operand types are 'Counter' and 'Counter')
11 | return value + value;
| ~~~~~~^~~~~~~
status 1
The Rust version of the same body is rejected where it is written, because no bound allows the addition. The error names the operator and the parameter type, and the fix is the bound in the signature.
fn twice<T>(value: T) -> T {
value + value
}
fn main() {
println!("{}", twice(21));
}
rustc --edition 2024 templates_wrong.rs
error[E0369]: cannot add `T` to `T`
--> templates_wrong.rs:2:11
|
2 | value + value
| ----- ^ ----- T
| |
| T
|
help: consider restricting type parameter `T` with trait `Add`
|
1 | fn twice<T: std::ops::Add<Output = T>>(value: T) -> T {
| +++++++++++++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0369`.
The C++ lesson for this ground is Function templates, and the Rust lesson is Generic functions and structs. The bounds themselves are the next Rust lesson, Trait bounds.
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