Featured image of post Implementing Rust-style Result Type in C++

Implementing Rust-style Result Type in C++

Implementing Rust language's Result type in C++ for returning and propagating errors

Introduction

Rust’s Result type is an elegant error handling mechanism that forces developers to explicitly handle both success and failure states, avoiding the uncertainty brought by unchecked exceptions. In C++, although similar functionality can be implemented through exceptions or error codes, the Result type provides a more structured approach, especially suitable for projects that require clear error paths.

This article introduces a C++-based Result implementation inspired by Rust.

Analysis of Rust’s Result Type

In the Rust language, Result is a very important type used to represent the outcome of function execution. It allows developers to elegantly handle success and failure cases while avoiding common error handling problems in traditional programming languages.

It is an enum with variants, where Ok(T) represents success and contains a value, while Err(E) represents an error and contains an error value.

1
2
3
4
enum Result<T, E> {
   Ok(T),
   Err(E),
}

In Rust, it’s typically used like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Cannot divide by zero"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    let result = divide(10, 2);
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(err) => println!("Error: {}", err),
    }
}

Besides using match for pattern matching, there are several main methods:

1
2
let result = divide(10, 2);
println!("Result: {}", result.unwrap());  // Output: Result: 5

unwrap() method: If the Result is Ok, it returns the internal value; if it’s Err, it triggers a panic.

1
2
let result = divide(10, 0);
println!("Result: {}", result.unwrap_or(-1));  // Output: Result: -1

unwrap_or() method: If the Result is Ok, it returns the internal value; if it’s Err, it returns a default value.

Implementing Rust’s Result in C++

Rust’s Result with Ok(T) and Err(E) is a generic type. The initial implementation attempted to use std::variant<T, E> to store results but encountered a key issue: When T and E are of the same type, std::variant cannot distinguish between success and error values. For example, for Result<int, int>, the two alternatives in std::variant<int, int> have the same type, making it impossible to clearly identify the source of assignment. To address this, std::any is used to store values instead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// Result type, similar to Rust Result

#pragma once
#include <any>
#include <iostream>
#include <stdexcept>
#include <string>

// custom exception(optional)
class ResultException final : public std::runtime_error {
 public:
  explicit ResultException(const std::string& message) : std::runtime_error(message) {}
};

template <typename T, typename E = std::string>
class Result {
 private:
  // Using variant is actually better, but when T and E are of the same type, it can cause ambiguity.
  // std::variant<T, E> data;
  std::any ok_;
  std::any err_;

 public:
  // Construct a successful Result
  static Result Ok(T value) {
    Result result;
    result.ok_ = std::move(value);
    return result;
  }

  // Construct a wrong Result
  static Result Err(E error) {
    Result result;
    result.err_ = std::move(error);
    return result;
  }

  // Check if successful
  [[nodiscard]] bool is_ok() const { return ok_.has_value(); }

  // Check if error
  [[nodiscard]] bool is_err() const { return err_.has_value(); }

  // Get the successful value and throw an exception if it is an error
  T unwrap() const {
    if (is_err()) {
      throw ResultException("Unwrapped an Err value");
    }
    return std::any_cast<T>(ok_);
  }

  // Get the error value and throw an exception if successful
  E unwrap_err() const {
    if (is_ok()) {
      throw ResultException("Unwrapped_err on an Ok value");
    }
    return std::any_cast<E>(err_);
  }

  // Get values safely, provide default values
  T unwrap_or(T default_value) const { return is_ok() ? std::any_cast<T>(ok_) : default_value; }

  // Pattern matching style processing
  template <typename OkFunc, typename ErrFunc>
  auto match(OkFunc ok_func, ErrFunc err_func) const {
    if (is_ok()) {
      return ok_func(std::any_cast<T>(ok_));
    } else {
      return err_func(std::any_cast<E>(err_));
    }
  }
};

Here we implement a concise version, keeping only the basic functionality. It’s simple enough and won’t cause errors.

Implementation Analysis

Core structure:

1
2
3
4
5
6
7
template <typename T, typename E = std::string>
class Result {
 private:
  std::any ok_;
  std::any err_;
  // ...
};
  • ok_ and err_ store success and error values respectively, with state determined using has_value().

Key methods:

  1. Construction Methods

    1
    2
    
    static Result Ok(T value); // Construct success result
    static Result Err(E error); // Construct error result
    
  2. State Checking

    1
    2
    
    [[nodiscard]] bool is_ok() const;
    [[nodiscard]] bool is_err() const;
    
  3. Value Extraction

    • unwrap(): Returns the success value, throws ResultException if error.
    • unwrap_err(): Returns the error value, throws exception if success.
    • unwrap_or(T default_value): Safely extracts value, providing default fallback.
  4. Pattern Matching

    1
    2
    
    template <typename OkFunc, typename ErrFunc>
    auto match(OkFunc ok_func, ErrFunc err_func) const;
    

    Calls the corresponding function based on state, similar to Rust’s match syntax.

Usage

Usage is also straightforward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <print> // C++ 23

Result<int, std::string> divide(int numerator, int denominator) {
    if (denominator == 0) {
        return Result<int, std::string>::Err("Division by zero");
    }
    return Result<int, std::string>::Ok(numerator / denominator);
}

int main() {  
    // success
    auto success_result = divide(10, 2);
    success_result.match(
        [](int value) { std::println("Success: {}", value); },
        [](const std::string& err) { std::println("Error: {}", err); }
    );
    std::println("is_ok:{} is_err:{} unwrap:{}", success_result.is_ok(), success_result.is_err(),
    success_result.unwrap());
    // wrong
    auto error_result = divide(10, 0);
    error_result.match(
        [](int value) { std::println("Success: {}", value); },
        [](const std::string& err) { std::println("Error: {}", err); }
    );
    std::println("is_ok:{} is_err:{} unwrap_err:{}", error_result.is_ok(), error_result.is_err(),
    error_result.unwrap_err());

    // methond 2
    if(auto result = divide(10, 2); re.is_ok()) {
      std::println("Success: {}", re.unwrap());
    }
    else {
      std::println("Error: {}", re.unwrap_err());
    }
    return 0;
}

Conclusion

Rust’s Result is an excellent type that can effectively solve return value problems, capable of carrying either correct values or error information. The Result type implemented here using std::any provides a flexible error handling mechanism, especially suitable for scenarios where T and E types might be the same.

For C++ implementations of Result, there are many approaches, including open-source implementations like: https://github.com/oktal/result

https://en.cppreference.com/w/cpp/utility/variant

https://en.cppreference.com/w/cpp/utility/any

https://github.com/oktal/result

Licensed under CC BY-NC-SA 4.0