Featured image of post 在C++中实现Rust风格的Result类型

在C++中实现Rust风格的Result类型

使用C++实现Rust语言的Result类型,用于返回和传播错误

前言

Rust 的 Result 类型是一种优雅的错误处理机制,它强制开发者显式处理成功和失败两种状态,避免了未检查异常带来的不确定性。在 C++中,虽然可以通过异常或错误码实现类似功能,但 Result 类型提供了一种更结构化的方式,尤其适用于需要明确错误路径的项目。

本文介绍一个基于 C++的 Result 实现,其设计灵感来自 Rust。

Rust 的 Result 类型分析

在 Rust 语言中,Result 是一种非常重要的类型,用于表示函数执行的结果,它允许开发者优雅地处理成功和失败的情况,同时避免了传统编程语言中常见的错误处理问题。

它是一个带有变体的枚举,Ok(T) 表示成功并包含一个值,而 Err(E) 表示错误并包含一个错误值。

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

在 Rust 中,通常这样使用:

 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),
    }
}

除了使用 match 匹配,还有几个主要的方法:

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

unwrap() 方法:如果 ResultOk,则返回内部的值;如果是 Err,则会触发 panic。

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

unwrap_or() 方法:如果 ResultOk,则返回内部的值;如果是 Err,则返回一个默认值。

C++的 Rust 实现

Rust 中的 Result 的 Ok(T)Err(E) 是一个范型,最初的实现尝试使用 std::variant<T, E> 存储结果,但遇到了一个关键问题:TE 类型相同时,std::variant 无法区分成功和错误值。例如,对于 Result<int, int>std::variant<int, int> 的两个备选项类型相同,导致无法明确赋值来源。为此,改用 std::any 存储值。

 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_));
    }
  }
};

这里实现一个简洁版本,只保留基本功能,足够简单,不会出错。

实现解析

核心结构:

1
2
3
4
5
6
7
template <typename T, typename E = std::string>
class Result {
 private:
  std::any ok_;
  std::any err_;
  // ...
};
  • ok_err_ 分别存储成功值和错误值,通过 has_value() 判断状态。

关键方法:

  1. 构造方法

    1
    2
    
    static Result Ok(T value); // 构造成功结果
    static Result Err(E error); // 构造错误结果
    
  2. 状态检查

    1
    2
    
    [[nodiscard]] bool is_ok() const;
    [[nodiscard]] bool is_err() const;
    
  3. 值提取

    • unwrap():返回成功值,若为错误则抛出 ResultException
    • unwrap_err():返回错误值,若为成功则抛出异常。
    • unwrap_or(T default_value):安全提取值,提供默认回退。
  4. 模式匹配

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

    根据状态调用对应的函数,类似于 Rust 的 match 语法。

用法

用法也很简单:

 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;
}

总结

Rust 的 Result 是一个不错的类型,可以有效解决返回值的问题,能够携带正确值或者错误信息。这里通过 std::any 实现的 Result 类型提供了一种灵活的错误处理机制,尤其适用于 TE 类型可能相同的场景。

对于 Result 的 C++实现,有很多方式,也有开源实现,比如: https://github.com/oktal/result

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

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

https://github.com/oktal/result

Licensed under CC BY-NC-SA 4.0