Let's look at some example of mathematics vector. It consists of a different number of components depending on the space dimension.
In C++ I can use SFINAE concept to implement it.
template <size_t D, typename T, typename = void>
struct Vector;
// Implement for 2D
template<size_t D, typename T>
struct Vector <D, T, std::enable_if_t<(D == 2)>>
{
T x;
T y;
}
// Implement for 3D
template<size_t D, typename T>
struct Vector <D, T, std::enable_if_t<(D == 3)>>
{
T x;
T y;
T z;
}
// Implement for 4D
template<size_t D, typename T>
struct Vector <D, T, std::enable_if_t<(D == 4)>>
{
T x;
T y;
T z;
T w;
}
How can I do the same in Rust?
You cannot specialize generics in Rust like specializing templates in C++. (Rust has a feature called "specialization", but it only applies to impl
s, and it's not really relevant here.) Rust generics are sometimes called "principled" because they have to work in principle (upon declaration), not just in practice (once instantiated). This is a deliberate choice on the part of Rust's designers to avoid some of the messier consequences of SFINAE in C++.
I can think of two main ways to achieve a similar effect to your C++ code in Rust, depending on the generic context of the code. One way is to use a trait as a type level function to compute the content type of a parameterized struct, which is similar to the C++ version but has slightly more verbose field access (for simplicity, I'll imagine that T
is f32
for these examples):
// types that contain the actual data
struct Vector2 {
x: f32,
y: f32,
}
struct Vector3 {
x: f32,
y: f32,
z: f32,
}
// types that will be used to parameterize a type constructor
struct Fixed<const N: usize>;
struct Dynamic;
// a type level function that says what kind of data corresponds to what type
trait VectorSize {
type Data;
}
impl VectorSize for Fixed<2> {
type Data = Vector2;
}
impl VectorSize for Fixed<3> {
type Data = Vector3;
}
impl VectorSize for Dynamic {
type Data = Vec<f32>;
}
// pulling it all together
struct Vector<Z>(Z::Data) where Z: VectorSize;
Now, if you have v: Vector<Fixed<2>>
you can use v.0.x
or v.0.y
, whereas if you have a Vector<Dynamic>
you have to use v.0[0]
and v.0[1]
. But there's no way to write a generic function that uses x
and y
that will work with either Vector<Fixed<2>>
or Vector<Fixed<3>>
; since there's no semantic relationship between those x
s and y
s, that would be unprincipled.
Another option would be putting an array in Vector
and making x
and y
convenience methods that access elements 0 and 1:
struct Vector<const N: usize> {
xs: [f32; N],
}
impl<const N: usize> Vector<N> {
fn x(&self) -> f32 where Self: SizeAtLeast<2> {
self.xs[0]
}
fn y(&self) -> f32 where Self: SizeAtLeast<2> {
self.xs[1]
}
fn z(&self) -> f32 where Self: SizeAtLeast<3> {
self.xs[2]
}
}
// In current Rust, you can't bound on properties of const generics, so you have
// to do something like this where you implement the trait for every relevant
// number. Macros can make this less tedious. In the future you should be able to
// simply add bounds on `N` to `x`, `y` and `z`.
trait SizeAtLeast<const N: usize> {}
impl SizeAtLeast<2> for Vector<2> {}
impl SizeAtLeast<2> for Vector<3> {}
impl SizeAtLeast<2> for Vector<4> {}
impl SizeAtLeast<3> for Vector<3> {}
impl SizeAtLeast<3> for Vector<4> {}
Now you can write generic functions that work for Vector<N>
and use x
and y
, but it's not as easy to adapt this to allow mutation. One way to do so is to add x_mut
, y_mut
and z_mut
methods that return &mut f32
.
Failing that, tuple types may become handy here. You could define the generic Vector
struct below, which is aimed at taking a tuple type as the generic type argument:
struct Vector<T> {
pub coord: T,
}
impl<T> Vector<T> {
fn new(coord: T) -> Self {
Self { coord }
}
}
Then, as an example, if (f32, f32)
is the type argument for Vector
, you will obtain a two-dimensional vector:
let v2d /* : Vector<(f32,f32)> */ = Vector::new((1.0, 2.0)); value
println!("v2 = ({}, {})", v2d.coord.0, v2d.coord.1);
Similarly for three dimensions (i.e., T = (f32, f32, f32)
):
type V3d = Vector<(f32, f32, f32)>;
let v3d = V3d::new((1.0, 2.0, 3.0));
...and so on.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With