5

I was in search of a function to increment a value of type T, and I stumbled upon the trait Saturating in the Substrate documentation.

Upon examining the saturating_inc() function, I found the signature quite intriguing:

fn saturating_inc(&mut self)
    where Self: One { ... }

This method increments the value of self, but if it's already at its maximum, it remains at that maximum value. This behavior is clear, but what surprises me is that it does so without any notification or error. In my specific use case, I would prefer to raise an error (such as a type overflow) if the value is at its maximum.

Could someone please help me understand why the saturating_inc() function is designed this way? What are the typical use cases for this behavior, and is there a way to detect when the saturation occurs if needed? Any insights or examples would be greatly appreciated.

2 Answers 2

3

The code you are looking for to increment x is most likely

let incremented = x.checked_add(1.into()).ok_or(MyError)?;

The primary reason that Saturating and Checked{Add/Div/Mul} traits are used in Substrate is that overflowing/underflowing in runtime code is really bad. Rust panics on overflow/underflow in debug mode, but will silently wrap arithmetic in release mode, as specified by RFC 560.

For saturating arithmetic, you are correct that there are not too many typical use cases for this behavior, and most of the time you do want the erroring variant (which is the CheckedAdd mentioned above). Although saturating instead of wrapping is typically less damaging, they both violate the consistency of your state, and preventing saturation and wrapping is much better. The Checked{x} traits provide signatures that return None on over/underflow, at which point you can error. Most of the time, this is what you want.

// From CheckedAdd
fn checked_add(&self, v: &Self) -> Option<Self>

Saturating arithmetic should only be used when you would otherwise need to include a check for overflow/underflow, and set it to the corresponding extreme value. The use cases below should make this clear.

Saturating Use Cases

This is copied from a good post on the rust forums:

Make the player lose 10 coins when they die, avoiding overflow:

player.coins = player.coins.saturating_sub(10);

Align some text to the middle, but if it is too large align it to the left edge:

let position = (total_width/2).saturating_sub(text_width/2);

Parse a digit into a number n, capping off too large numbers such that "8354" will be cut off at 255:

n = n.saturating_mul(10_u8).saturating_add(digit);

Move the cursor to the left unless it is at the left edge:

cursor.position.x = cursor.position.x.saturating_sub(1);
2

The saturating_ versions are used in cases where an overflow is not expected but would otherwise panic the runtime.

Nowadays you can use the defensive_ variants like defensive_saturating_inc which will log an error message and fail in tests but otherwise behave as usual.

3
  • I do want to make the point that overflows/underflows do not panic in release mode. You can test it yourself by creating a storage value with an extrinsic to decrement it, and running against it in release mode. Aug 2, 2023 at 17:40
  • 1
    Ah okay. I see there is a compiler flag to change it. For downstream code it can therefore be treated as possible panicker. Aug 2, 2023 at 19:05
  • 1
    Ah, that's a good point. For other people's reference, the flag is called overflow-checks. Aug 2, 2023 at 21:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.