Post-increment method on numeric types?

As a long time Rust user and C/C++ skeptic, I totally get why C-style pre- and post-increment operators were not included in Rust -- their behavior is subtle and confusing, and their usage is uncommon enough to not warrant the syntax complexity. That said, recently I've written a fair amount of code where it would have been convenient to have a post-increment method on usize, like:

impl usize {
    fn post_increment(&mut self, diff: Self) -> Self {
        let before = *self;
        *self += diff;
        before
    }
}

If one takes it as a given that this method is useful, the following methods seem valuable to maintain symmetry, in approximately decreasing order of importance:

  1. {integer}.post_increment(&mut self, diff: Self) -> Self on other integral types.
  2. {integer}.post_decrement(&mut self, diff: Self) -> Self.
  3. {integer}.pre_(increment|decrement) (&mut self, diff: Self) -> Self.
  4. {float}.(pre|post)_(increment|decrement) (&mut self, diff: Self) -> Self.

All of these methods seem pretty straightforward, with obvious semantics and implementations.

(Aside: one might also desire variants of these methods for wrapping, saturating, checked &c arithmetic, but I'm less concerned about that.)

Have these methods been considered before? Is there any reason not to include them?

1 Like

@Centril points out (offline) that post_add (and presumably post_sub) might be a better name for this method. This seems reasonable to me, but names are not terribly interesting to me at this point. Please imagine your ideal name(s) for this method (/ these methods), and respond accordingly.

Can you say more about where you wanted to use these? Given that most loops in Rust use .. instead of ++, I can't say I've really missed them in the same way, especially since expression block scopes are a thing.

Where is it unacceptably painful to just put the += 1 on the next line?

2 Likes

Note that my proposed methods do not increment by 1; they take an argument by which to increment. I've used the more-verbose equivalent of post-increment recently when implementing the size_of and align_of phases of a compiler, which amount to iterating over a product's elements and incrementing an offset by the element's size. It wasn't unacceptably painful to write:

let my_offset = *current_offset;
*current_offset += my_size;

but it would have been more convenient to instead write:

let my_offset = current_offset.post_increment(my_size);

This is not a huge gripe, but it also doesn't seem like there would be a huge cost to make it go away.

A post-increment method which statically incremented by exactly one would be silly; you're right. for loops accomplish that fine.

These "post" methods already exist on atomics, e.g. AtomicUsize::fetch_add. But there they take an ordering as well. Also, std::mem::replace is similar.

2 Likes

Why not just make a trait with a blanket impl for T: AddAssign + Copy that does what you want?

I am often tempted to use std::mem::replace to do stuff like this but I think code that does more than one "thing" on a line is hard to read and maintain.

3 Likes

I could easily implement my own AddAssign trait, but that occupies an awkward space where it seems to trivial to publish as a library and depend on in the handful of projects where I'd want it, but too much boilerplate to want to duplicate in each of those projects.

In my opinion, post-increment is at least as useful as some of the operators currently exposed on integral types (e.g. borrowing_sub, next_multiple_of, ilog10). This is not to say that those operations don't belong in the standard library; I think they do; it's that I think post-increment also belongs in the standard library.

2 Likes

I've occasionally had to write code in this form, when needing an iota like sequential IDs.

A general solution:

trait CombinatorRet {
    fn ret<T>(&self, x: T) -> T {
        x
    }
}

trait CombinatorThen: Sized {
    fn then<T>(self, _: T) -> Self {
        self
    }
}

impl<T> CombinatorRet for T {}
impl<T: Sized> CombinatorThen for T {}

fn foo(mut x: i32) -> i32 {
    x.then(x += 1).then(println!("{}", x))
}

fn bar(mut x: i32) -> i32 {
    (x += 1).ret(x).then(x += 1).then(println!("{}", x))
}

fn main() {
    println!("{}, {}", foo(1), bar(1));
}