Private constructors
In C++ constructors for classes can be made private by declaring them private,
or by defining a class using class
and using the default private visibility.
In Rust, constructors (the actual constructors, not "constructor methods") for structs are visible from wherever the type and all fields are visible. To achieve similar visibility restrictions as in the C++ example, an additional private field needs to be added to the struct in Rust. Because Rust supports zero-sized types, the additional field can have no performance cost. The unit type has zero size and can be used for this purpose.
#include <string>
struct Person {
std::string name;
int age;
private:
Person() = default;
};
int main() {
// fails to compile, Person::Person() private
// Person nobody;
// fails to compile since C++20
// Person alice{"Alice", 42};
return 0;
}
mod person {
pub struct Person {
pub name: String,
pub age: i32,
_private: (),
}
impl Person {
pub fn new(
name: String,
age: i32,
) -> Person {
Person {
name,
age,
_private: (),
}
}
}
}
use person::*;
fn main() {
// field `_private` of struct `person::Person`
// is private
// let alice = Person {
// name: "Alice".to_string(),
// age: 42,
// _private: (),
// };
// cannot construct `person::Person` with
// struct literal syntax due to private fields
// let bob = Person {
// name: "Bob".to_string(),
// age: 55,
// };
let carol =
Person::new("Carol".to_string(), 20);
// Can match on the public fields, and then
// use .. to ignore the remaning ones.
let Person { name, age, .. } = carol;
}
Enums
Unlike C++ unions, but like std::variant
, Rust enums do not have direct
control over the visibility of their variants or the fields of their variants.
In the following example, the circle
variant of the Shape
union is not
public, so it can only be accessed from within the definition of Shape
, as it
is by the make_circle
static method.
#include <iostream>
struct Triangle {
double base;
double height;
};
struct Circle {
double radius;
};
union Shape {
Triangle triangle;
private:
Circle circle;
public:
static Shape make_circle(double radius) {
Shape s;
s.circle = Circle(radius);
return s;
};
};
int main() {
Shape triangle;
triangle.triangle = Triangle{1.0, 2.0};
Shape circle = Shape::make_circle(1.0);
// fails to compile
// circle.circle = Circle{1.0};
// fails to compile
// std::cout << shape.circle.radius;
}
In Rust visibility modifiers cannot be applied to individual enum variants or their fields.
mod shape {
pub enum Shape {
Triangle { base: f64, height: f64 },
Circle { radius: f64 },
}
}
use shape::*;
fn main() {
// Variant constructor is accesssible despite not being marked pub.
let triangle = Shape::Triangle {
base: 1.0,
height: 2.0,
};
let circle = Shape::Circle { radius: 1.0 };
// Fields accessbile despite not being marked pub.
match circle {
Shape::Triangle { base, height } => {
println!("Triangle: {}, {}", base, height);
}
Shape::Circle { radius } => {
println!("Circle {}", radius);
}
}
}
Instead, to control construction of and pattern matching on the enum implementation, one of two approaches can be taken. The first controls construction of and access to the fields, but not inspection of which variant is active.
mod shape {
pub struct Triangle {
pub base: f64,
pub height: f64,
_private: (),
}
pub struct Circle {
pub radius: f64,
_private: (),
}
pub enum Shape {
Triangle(Triangle),
Circle(Circle),
}
impl Shape {
pub fn new_triangle(base: f64, height: f64) -> Shape {
Shape::Triangle(Triangle {
base,
height,
_private: (),
})
}
pub fn new_circle(radius: f64) -> Shape {
Shape::Circle(Circle {
radius,
_private: (),
})
}
}
}
use shape::*;
fn main() {
let triangle = Shape::new_triangle(1.0, 2.0);
let circle = Shape::new_circle(1.0);
match circle {
Shape::Triangle(Triangle { base, height, .. }) => {
println!("Triangle: {}, {}", base, height);
}
Shape::Circle(Circle { radius, .. }) => {
println!("Circle: {}", radius);
}
}
}
The second places the enum in a struct with a private field, preventing both construction and inspection from outside of the module.
mod shape {
enum ShapeKind {
Triangle { base: f64, height: f64 },
Circle { radius: f64 },
}
pub struct Shape(ShapeKind);
impl Shape {
pub fn new_circle(radius: f64) -> Shape {
Shape(ShapeKind::Circle { radius })
}
pub fn new_triangle(base: f64, height: f64) -> Shape {
Shape(ShapeKind::Triangle { base, height })
}
pub fn print(&self) {
match self.0 {
ShapeKind::Triangle { base, height } => {
println!("Triangle: {}, {}", base, height);
}
ShapeKind::Circle { radius } => {
println!("Circle: {}", radius);
}
}
}
}
}
use shape::*;
fn main() {
let triangle = Shape::new_triangle(1.0, 2.0);
let circle = Shape::new_circle(1.0);
// Does not compile because Shape has private fields.
// match circle {
// Shape(_) -> {}
// }
circle.print();
}
If the purpose of making the variants private is to ensure that invariants are
met, then it can be useful to expose the implementing enum (ShapeKind
) but not
the field of the wrapping struct (Shape
), with the invariants only being
guaranteed when the wrapping struct is used. In this case, it is necessary to
make the field private and define a getter function, since otherwise the field
would be modifiable, possibly violating the invariant that the wrapping struct
represents.
mod shape {
pub enum ShapeKind {
Triangle { base: f64, height: f64 },
Circle { radius: f64 },
}
// The field of Shape is private.
pub struct Shape(ShapeKind);
impl Shape {
pub fn new(kind: ShapeKind) -> Option<Shape> {
// ... check invariants ...
Some(Shape(kind))
}
pub fn get_kind(&self) -> &ShapeKind {
&self.0
}
}
}
use shape::*;
fn main() {
let triangle = Shape::new(ShapeKind::Triangle {
base: 1.0,
height: 2.0,
});
let Some(circle) = Shape::new(ShapeKind::Circle { radius: 1.0 }) else {
return;
};
// Does not compile because Shape has private fields.
// match circle {
// Shape(c) => {}
// };
match circle.get_kind() {
ShapeKind::Triangle { base, height } => {
println!("Triangle: {}, {}", base, height);
}
ShapeKind::Circle { radius } => {
println!("Circle: {}", radius);
}
}
}
The situation in Rust resembles the situation in C++ when using std::variant
,
for which it is not possible to make the variants themselves private. Instead
either the constructors for the types that form the variants can be made private
or the variant can be wrapped in a class with appropriate visibility controls.
Rust's #[non_exhaustive]
annotation
If a struct or enum is intended to be public within a
crate, but
should not be constructed outside of the crate, then the #[non_exhaustive]
attribute can be used to constrain construction. The attribute can be applied to
both structs and to individual enum variants with the same effect as adding a
private field.
However, the attribute applies the constraint at the level of the crate, not at the level of a module.
The attribute is more typically used to force clients of a library to include the wildcard when matching on the struct fields, making it so that adding additional fields to a struct is not breaking change (i.e., that it does not require the increase of the major version component when using semantic versioning).
Applying the #[non_exhaustive]
attribute to the enum itself makes it as if one
of the variants were private, requiring a wildcard when matching on the variant
itself. This has the same effect in terms of versioning as when used on a struct
but is less advantageous. In most cases, code failing to compile when a new enum
variant is added is desirable, since that indicates a new case that requires
handling logic.