Constructors
In C++, constructors initialize objects. At the point when a constructor is executed, storage for the object has been allocated and the constructor is only performing initialization.
Rust does not have constructors in the same way as C++. In Rust, there is a
single fundamental way to create an object, which is to initialize all of its
members at once. The term "constructor" or "constructor method" in Rust refers
to something more like a factory: a static method associated with a type (i.e.,
a method that does not have a self
parameter), which returns a value of the
type.
#include <thread>
unsigned int cpu_count() {
return std::thread::hardware_concurrency();
}
class ThreadPool {
unsigned int num_threads;
public:
ThreadPool() : num_threads(cpu_count()) {}
ThreadPool(unsigned int nt) : num_threads(nt) {}
};
int main() {
ThreadPool p1;
ThreadPool p2(4);
}
fn cpu_count() -> usize { std::thread::available_parallelism().unwrap().get() } struct ThreadPool { num_threads: usize } impl ThreadPool { fn new() -> Self { Self { num_threads: cpu_count() } } fn with_threads(nt: usize) -> Self { Self { num_threads: nt } } } fn main() { let p1 = ThreadPool::new(); let p2 = ThreadPool::with_threads(4); }
In Rust, typically the primary constructor for a type is named new
, especially if it
takes no arguments. (See the chapter on default
constructors.) Constructors based on
some specific property of the value are usually named with_<something>
, e.g.,
ThreadPool::with_threads
. See the naming
guidelines for the
conventions on how to name constructor methods in Rust.
If the fields to be initialized are visible, there is a reasonable default value, and the value does not manage a resource, then it is also common to use record update syntax to initialize a value based on some default value.
struct Point { x: i32, y: i32, z: i32, } impl Point { const fn zero() -> Self { Self { x: 0, y: 0, z: 0 } } } fn main() { let x_unit = Point { x: 1, ..Point::zero() }; }
Despite the name, "record update syntax" does not modify a record but instead creates a new value based on another one, taking ownership of it in order to do so.
Storage allocation vs initialization
In Rust, the actual construction of a structure or enum value occurs where the
structure construction syntax ThreadPool { ... }
is, after the evaluation of the
expressions for the fields.
A significant implication of this difference is that storage is not allocated
for a struct in Rust at the point where the constructor method (such as
ThreadPool::with_threads
) is called, and in fact is not allocated until after the
values of the fields of a struct have been computed (in terms of the semantics
of the language — the optimizer may still avoid the copy). Therefore there is no
straightforward way in Rust to translate patterns such as a class which stores a pointer to
itself upon construction (in Rust, this requires tools like Pin
and MaybeUninit
).
Fallible constructors
In C++, the primary way constructors can indicate failure is by throwing exceptions. In Rust, because constructors are normal static methods, fallible constructors
can instead return Result
(akin to std::expected
) or Option
(akin to
std::optional
).
#include <iostream>
#include <stdexcept>
class ThreadPool {
unsigned int num_threads;
public:
ThreadPool(unsigned int nt) : num_threads(nt) {
if (num_threads == 0) {
throw std::domain_error("Cannot have zero threads");
}
}
};
int main() {
try {
ThreadPool p(0);
} catch (const std::domain_error &e) {
std::cout << e.what() << std::endl;
}
}
struct ThreadPool { num_threads: usize, } impl ThreadPool { fn with_threads(nt: usize) -> Result<Self, String> { if nt == 0 { Err("Cannot have zero threads".to_string()) } else { Ok(Self { num_threads: nt }) } } } fn main() { match ThreadPool::with_threads(0) { Err(err) => println!("{err}"), Ok(p) => { /* ... */ } } }
See the chapter on exceptions for more information on how C++ exceptions and exception handling translate to Rust.
Click here to leave us feedback about this page.