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
enumResult<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
fndivide(a: i32,b: i32)-> Result<i32,String>{ifb==0{Err(String::from("Cannot divide by zero"))}else{Ok(a/b)}}fnmain(){letresult=divide(10,2);matchresult{Ok(value)=>println!("Result: {}",value),Err(err)=>println!("Error: {}",err),}}
Besides using match for pattern matching, there are several main methods:
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.
// Result type, similar to Rust Result
#pragma once
#include<any>#include<iostream>#include<stdexcept>#include<string>// custom exception(optional)
classResultExceptionfinal:publicstd::runtime_error{public:explicitResultException(conststd::string&message):std::runtime_error(message){}};template<typenameT,typenameE=std::string>classResult{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::anyok_;std::anyerr_;public:// Construct a successful Result
staticResultOk(Tvalue){Resultresult;result.ok_=std::move(value);returnresult;}// Construct a wrong Result
staticResultErr(Eerror){Resultresult;result.err_=std::move(error);returnresult;}// Check if successful
[[nodiscard]]boolis_ok()const{returnok_.has_value();}// Check if error
[[nodiscard]]boolis_err()const{returnerr_.has_value();}// Get the successful value and throw an exception if it is an error
Tunwrap()const{if(is_err()){throwResultException("Unwrapped an Err value");}returnstd::any_cast<T>(ok_);}// Get the error value and throw an exception if successful
Eunwrap_err()const{if(is_ok()){throwResultException("Unwrapped_err on an Ok value");}returnstd::any_cast<E>(err_);}// Get values safely, provide default values
Tunwrap_or(Tdefault_value)const{returnis_ok()?std::any_cast<T>(ok_):default_value;}// Pattern matching style processing
template<typenameOkFunc,typenameErrFunc>automatch(OkFuncok_func,ErrFuncerr_func)const{if(is_ok()){returnok_func(std::any_cast<T>(ok_));}else{returnerr_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.
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.
Update:
The good news is that in C + + 23, std::expected type has been added, which is the official Rust-like Result type.
enumclassparse_error{invalid_input,overflow};autoparse_number(std::string_view&str)->std::expected<double,parse_error>{constchar*begin=str.data();char*end;doubleretval=std::strtod(begin,&end);if(begin==end)returnstd::unexpected(parse_error::invalid_input);elseif(std::isinf(retval))returnstd::unexpected(parse_error::overflow);str.remove_prefix(end-begin);returnretval;}intmain(){autoprocess=[](std::string_viewstr){std::cout<<"str: "<<std::quoted(str)<<", ";if(constautonum=parse_number(str);num.has_value())std::cout<<"value: "<<*num<<'\n';// If num did not have a value, dereferencing num
// would cause an undefined behavior, and
// num.value() would throw std::bad_expected_access.
// num.value_or(123) uses specified default value 123.
elseif(num.error()==parse_error::invalid_input)std::cout<<"error: invalid input\n";elseif(num.error()==parse_error::overflow)std::cout<<"error: overflow\n";elsestd::cout<<"unexpected!\n";// or invoke std::unreachable();
};for(autosrc:{"42","42abc","meow","inf"})process(src);}
预览: