Rust’s Copy trait – An example of a Vec inside a struct

While implementing a very primitive molecular dynamics simulator from scratch in Rust, I have encountered an interesting corner case I believe is worth sharing with anyone learning Rust.

Among other artifacts, I have set up a primitive model class for storing some information about a single Particle in a file particle.rs:

#[derive(Debug, Copy, Clone)]
pub enum SubatomicParticleType {
    Proton = 1,
    Neutron = 2,
    Electron = 3,
}

#[derive(Debug, Copy, Clone)]
pub struct Particle {
    pub pos: Vector3<f64>, // current Euclidean XYZ position [m]
    pub v: Vector3<f64>,   // current Euclidean XYZ accelleration [m/s]
    pub a: Vector3<f64>,   // current Euclidean XYZ accelleration [m/s^2]
    pub m: f64,            // particle mass [kg]
    pub r: f64,            // radius of the particle [m]
    pub e: f64,            // potential energy [kcal/mol]
    pub q: f64,            // electrical charge [C]
    pub particle_type: SubatomicParticleType,
}

Nothing fancy, just some basic properties like position, velocity, mass, charge, etc. Besides that, in a file atom.rs I have a basic definition of a single atom (nucleus + electrons which orbit it) and a method to create hydrogen atom:

#[derive(Debug)]
pub struct Atom {
    pub electrons: Vec<particle::Particle>,
    // nucleus is represented as a single particle with a charge
    // equal to the atomic number * charge of a proton
    pub nucleus: particle::Particle,
}

impl Atom {
    pub fn create_hidrogen(location: Vector3<f64>) -> Self {
        // According to: https://www.sciencefocus.com/science/whats-the-distance-from-a-nucleus-to-an-electron/
        // the electron (if it was a particle, hehe) orbits the nucleus at a distance of 1/20 nanometers
        let offset = Vector3::new(0.05e-9, 0.0, 0.0);
        let electron = particle::Particle::create_electron(location + offset);
        Self {
            nucleus: particle::Particle::create_proton(location),
            electrons: vec![electron],
        }
    }
}

The main simulation controller is implemented in file simulation.rs:

pub struct Simulation {
    delta_t: f64,
    pub particles: Vec<particle::Particle>,
    temperature: f64,
}

impl Simulation {
  // deltaT - simulation timestamp [fs]
  pub fn new(delta_t: f64, temperature: f64) -> Self {
      Simulation {
          delta_t: delta_t,
          particles: Vec::new(),
          temperature: temperature,
      }
  }

  pub fn step(self: &mut Self) {
    ...
  }
    
  pub fn add_atom(self: &mut Self, atom: &atom::Atom) {
    for particle in &atom.electrons {
      self.particles.push(*particle);
    }
    self.particles.push(atom.nucleus);
  }
}

This code compiles without errors.

Now, let’s focus on the add_atom function. Notice that de-referencing of *particle when adding it to the self.particles vector? Ugly, right?

At first I wanted to avoid references altogether, so my C++ mindset went something like this:

    pub fn add_atom(self: &mut Self, atom: &atom::Atom) {
        for particle in atom.electrons {
            self.particles.push(particle);
        }
        self.particles.push(atom.nucleus);
    }

The error I got after trying to compile this was:

So, what’s happening here? I was trying to iterate over electrons in a provided atom by directly accessing the value of a member property electrons of an instance atom of type &atom::Atom. There are two ways my loop can get the value of the vector behind that property: moving the ownership or copying it. As the brilliant Rust compiler correctly pointed out, this property doesn’t implement Copy trait (since it’s a Vec<T>), so copying is not possible. The only remaining way to get a value behind it is to move the ownership from a function parameter into a temporary loop variable.

Now, this isn’t possible either because you can’t move ownership of something behind a shared reference. This is why I’ve been left with the ugly de-referencing shown in the first place. Besides, I had to mark Particle with Copy and Clone traits as well.

Expanding a struct with a Vec<T> member property

In order to record historical data for plotting purposes about a particle’s trajectory through space, forces acting on it, its velocities, etc. I wanted to add a HashMap of vectors to the Particle struct, so the string keys represent various properties I need the history for. So, my Particles struct looked something like this:

Rust didn’t like this new HashMap of vectors due to the reason we already went over above – vectors can’t implement Copy traits. If I really wanted to keep this property the way it is, I would have to remove the Copy trait from the Particle struct. As you may already assume, this lead to another issue, this time in simulation.rs:

By removing the Copy trait on Particle struct we removed the capability for it to be moved by de-referencing. Since we must provide ownership to the each element of the vector self.particles, the only option is to clone each element explicitly before pushing it to the vector:

This code will finally compile and do what I need it to do. Yaaaay!

In cases like this Rust’s borrow checker can be described as annoying at first, but it does force you as a developer to take care of the underlying memory on time. For instance, de-referencing a pointer in C++ will almost never stop you from compiling, but you have to pray to the Runtime Gods nothing goes wrong. Rust, on the other hand, will force you to think about is it possible to de-reference this without any issues in all of the cases or not, and if not it will scream at you until you change your approach about it. In the example above I had to accept the fact my particle will be cloned physically instead of just getting a quick and dirty access to it through a reference, which is great.

Fighting the compiler can get rough at times, but at the end of the day the overhead you pay is a very low price for all of the runtime guarantees.

Leave a Comment