Optional return values

One idiom in C++ for optionally producing a result from a method or function is to use a reference parameter along with a boolean or integer return value to indicate whether the result was produced. This might be done for the same reasons as for using out parameters for multiple return values:

  • compatibility with versions of C++ earlier than C++11,
  • working in a codebase that uses C-style of C++, and
  • performance concerns.

The idiomatic Rust approach for optionally returning a value is to return a value of type Option.

#include <iostream>

bool safe_divide(unsigned int dividend,
                 unsigned int divisor,
                 unsigned int &quotient) {
  if (divisor != 0) {
    quotient = dividend / divisor;
    return true;
  } else {
    return false;
  }
}

void go(unsigned int dividend,
        unsigned int divisor) {
  unsigned int quotient;
  if (safe_divide(dividend, divisor, quotient)) {
    std::cout << quotient << std::endl;
  } else {
    std::cout << "Division failed!" << std::endl;
  }
}

int main() {
  go(10, 2);
  go(10, 0);
}
fn safe_divide(
    dividend: u32,
    divisor: u32,
) -> Option<u32> {
    if divisor != 0 {
        Some(dividend / divisor)
    } else {
        None
    }
}

fn go(dividend: u32, divisor: u32) {
    match safe_divide(dividend, divisor) {
        Some(quotient) => {
            println!("{}", quotient);
        }
        None => {
            println!("Division failed!");
        }
    }
}

fn main() {
    go(10, 2);
    go(10, 0);
}

When there is useful information to provide in the failing case, the Result type can be used instead. The chapter on error handling describes the use of Result.

Returning a pointer

When the value being returned is a pointer, another common idiom in C++ is to use nullptr to represent the optional case. In the Rust translation of that idiom, Option is also used, along with a reference type, such as & or Box. See the chapter on using nullptr as a sentinel value for more details.

Problems with the direct transliteration

It is possible to transliterate the original example that uses out parameters to Rust, but the resulting code is not idiomatic.

// NOT IDIOIMATIC RUST
fn safe_divide(dividend: u32, divisor: u32, quotient: &mut u32) -> bool {
    if divisor != 0 {
        *quotient = dividend / divisor;
        true
    } else {
        false
    }
}

fn go(dividend: u32, divisor: u32) {
    let mut quotient: u32 = 0; // initliazed to arbitrary value
    if safe_divide(dividend, divisor, &mut quotient) {
        println!("{}", quotient);
    } else {
        println!("Division failed!");
    }
}

fn main() {
    go(10, 2);
    go(10, 0);
}

This shares the same problems as with using out-parameters for multiple return values.

Similarities with C++ since C++17

C++17 and later offer std::optional, which can be used to express optional return values in a way similar to the idiomatic Rust example.

#include <iostream>
#include <optional>

std::optional<unsigned int> safe_divide(unsigned int dividend,
                                        unsigned int divisor) {
  if (divisor != 0) {
    return std::optional<unsigned int>(dividend / divisor);
  } else {
    return std::nullopt;
  }
}

void go(unsigned int dividend, unsigned int divisor) {
  if (auto quotient = safe_divide(dividend, divisor)) {
    std::cout << *quotient << std::endl;
  } else {
    std::cout << "Division failed!" << std::endl;
  }
}

int main() {
  go(10, 2);
  go(10, 0);
}

Helpful Option utilities

Rust provides several syntactic sugars for simplifying use of functions that return Option. If a failure should be propagated to the caller, then use the ? operator:

#![allow(unused)]
fn main() {
fn safe_divide(dividend: u32, divisor: u32) -> Option<u32> {
    if divisor != 0 {
        Some(dividend / divisor)
    } else {
        None
    }
}

fn go(dividend: u32, divisor: u32) -> Option<()> {
    let quotient = safe_divide(dividend, divisor)?;
    println!("{}", quotient);
    Some(())
}
}

If None should not be propagated, it is sometimes clearer to use let-else syntax:

fn safe_divide(dividend: u32, divisor: u32) -> Option<u32> {
    if divisor != 0 {
        Some(dividend / divisor)
    } else {
        None
    }
}

fn go(dividend: u32, divisor: u32) {
    let Some(quotient) = safe_divide(dividend, divisor) else {
        println!("Division failed!");
        return;
    };
    println!("{}", quotient);
}

fn main() {
    go(10, 2);
    go(10, 0);
}

If there is a default value that should be used in the None case, the Option::unwrap_or, Option::unwrap_or_else, Option::unwrap_or_default, or Option::unwrap methods can be used:

fn safe_divide(dividend: u32, divisor: u32) -> Option<u32> {
    if divisor != 0 {
        Some(dividend / divisor)
    } else {
        None
    }
}

fn expensive_computation() -> u32 {
    // ...
   0
}

fn go(dividend: u32, divisor: u32) {
    // If None, returns the given value.
    let result = safe_divide(dividend, divisor).unwrap_or(0);

    // If None, returns the result of calling the given function.
    let result2 = safe_divide(dividend, divisor).unwrap_or_else(expensive_computation);

    // If None, returns Default::default(), which is 0 for u32.
    let result3 = safe_divide(dividend, divisor).unwrap_or_default();

    // If None, panics. Prefer the other methods!
    // let result3 = safe_divide(dividend, divisor).unwrap();
}

fn main() {
    go(10, 2);
    go(10, 0);
}

In performance-sensitive code where you have manually checked that the result is guaranteed to be Some, Option::unwrap_unchecked can be used, but is an unsafe method.

There are additional utility methods that enable concise handling of Option values, which this book covers in the chapter on exceptions and error handling.

An alternative approach

An alternative approach in Rust to returning optional values is to require that the caller of a function prove that the value with which they call a function will not result in the failing case.

For the above safe division example, this involves the caller guaranteeing that the provided divisor is non-zero. In the following example this is done with a dynamic check. In other contexts the evidence needed may be available statically, provided from callers further upstream, or used more than once. In those cases, this approach reduces both runtime cost and code complexity.

use std::convert::TryFrom;
use std::num::NonZero;

fn safe_divide(dividend: u32, divisor: NonZero<u32>) -> u32 {
    // This is more efficient because the overflow check is skipped.
    dividend / divisor
}

fn go(dividend: u32, divisor: u32) {
    let Ok(safe_divisor) = NonZero::try_from(divisor) else {
        println!("Can't divide!");
        return;
    };

    let quotient = safe_divide(dividend, safe_divisor);
    println!("{}", quotient);
}

fn main() {
    go(10, 2);
    go(10, 0);
}