Introduction

Error handling in C++ can be done employing a large variety of techniques. I tried many of them, e.g. integer return codes, enums and exceptions. I ended up going for an enum class return value solution with exceptions here and there, for quite a while.

Consequently, I was able to write clean functions with significant error provision in most of the cases.

Problem

But what, if I want to return a value in case there’s no error?

Even more, reading The C++ Programming Language as well as the technical report (www.open-std.org/jtc1/sc22/wg21/docs/TR18015.pdf) and dozens of other sources, tweets and blog posts about exceptions in C++, I feared a performance bottleneck in my code, if exceptions are used to “break out” of a functional-chain in a controlled manner, hence, many exceptions are thrown.

This is where I ran into objections with my approach.

SDKs like DirectX or Vulkan and C-APIs in general usually expect a pointer, pointer-to-pointer or reference so that the result value can be written to that [out]-parameter from within the function, e.g:

VkResult vkCreateGraphicsPipelines(
    VkDevice                            device,
    VkPipelineCache                     pipelineCache,
    uint32_t                            createInfoCount,
    const VkGraphicsPipelineCreateInfo* pCreateInfos,
    const VkAllocationCallbacks*        pAllocator,
    VkPipeline*                         pPipelines); 

// Output pointer pPipelines, yielding nullptr or an array of 1..N VkPipeline objects.

I don’t like that kind of interfaces for several reasons.

  1. Function signature bloating: What if we have multiple values we want to return? We could either encapsulate all values in a struct OR have lot of [out]-parameters. The latter will bloat with every output value required and make the function signature very complex.
  2. Argument assertions: If pointers are used for [out]-parameters, we have to make sure they are not nullptr, so that dereferencing/assigning to the pointer/field will not cause undefined behaviour. As people make mistakes, this is error-prone.

It also does not solve the std::exception issues.

Working with C-APIs I learned to love consistent signatures and returning error codes/values from the functions, backed by a very detailed list of error codes and messages. But I also wanted to be able to return values in conjunction with the error code, so that I don’t have to employ [out]-Parameters.

Even more, I wanted to get rid of exceptions.

Monadic Interfaces & Optional

Using Rust for quite a while, I ran into std::result (https://doc.rust-lang.org/std/result/).

enum Result<T, E>
{
   Ok(T),
   Err(E),
}

This construct provides a monadic interface (https://en.wikipedia.org/wiki/Monad_(functional_programming)) to store a positive optional value of type T as OK(T) or an error value Err(E) with error type E.

Using the match-construct of Rust, this either-or approach is awesome to handle errors.

let version = parse_version(&[1, 2, 3, 4]);
match version 
{
    Ok(v)  => println!("working with version: {:?}", v),
    Err(e) => println!("error parsing header: {:?}", e),
}

However, it is Rust and not C++.

Meet std::expected<T, E>

Googlin’ around I discovered the subsequent proposals, which are the closest to Rust’s std::result I could find at first sight:

(C++ Monad Interfaces) http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0650r2.pdf

(C++ std::expected type) www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4109.pdf

Initially, I was quite happy to read these thinking:

Woah, I can use this for my error handling

The proposal states that:

Class template expected<T, E> proposed here is a type that may contain a value of type T or a value of type E in its storage space. T represents the expected value, E represents the reason explaining why it doesn’t contain a value of type T, that is the unexpected value.

WG21 – N4109

The template can be used like this:

enum class EMathError 
{ 
    Ok,
    DivideBy0 
};

std::expected<double, EError> safeDivide(double const &aI, double const &aJ)
{
    if(0 == aJ) 
    {
        return std::make_unexpected(EMathError::DivideBy0); // Implicit conversion E -> expected<T, E>
    }
    else 
    {
        return (aI / aJ); // Implicit conversion T -> expected<T, E>
    }
}

On second sight, however, I became reluctant to use it. Properly observing the definition, I realized that std::expected allows me to store either a value of type T OR an error description.

In my code base I define non-binary error model, though. There will be positive error codes, neutral codes as well as real error codes.

Consequently, I always have to provide both, value and error code, yielding std::expected to not being appropriate for my use case.

Writing my own Result-Type

In the quest for proper error handling, I decided to write my own result type, as provided below.

template <typename TResult, typename TData>
class AResult
{
public: // Constructors
    inline AResult(TResult const &aResult)
        : mResult(aResult)
        , mData(TData())
    {}
    
    inline AResult(TResult const &aResult, TData const &aData)
        : mResult(aResult)
        , mData(aData)
    {}
   
    inline AResult(TResult const &aResult, TData &&aData)
        : mResult(aResult)
        , mData(std::move(aData))
    {}

public: // Destructors
    virtual ~AResult() = default;

public: // Methods
    inline TResult const &result() const
    {
        return mResult;
    }

    inline TData const &data() const
    {
        return mData;
    }

    inline TData &data()
    {
        return mData;
    }

    virtual bool successful() const = 0;

private: // Members
    TResult mResult;
    TData   mData;
};

// Specialization for "result code only" cases
template <typename TResult>
class AResult<TResult, void>
{
public: // Constructors
    inline AResult(TResult const &aResult)
        : mResult(aResult)
    {}

public: // Destructors
    virtual ~AResult() = default;

public: // Methods
    inline TResult const &result() const
    {
        return mResult;
    }
    
    virtual bool successful() const = 0;

private: // Members
    TResult mResult;
};

The class stores a result code of type TResult together with a value of type TData. Initially, I thought about using std::optional<T> to store that data, which failed with non-copyable types (std::packaged_task or std::unique_ptr<T, D>).

As such, I just use the type as is and provide a copy and move constructor to move non-copyable values into the result type.

One important thing, though: The AResult abstract type can not be used directly, since the successful() -> bool {const} function has to be implemented, which yields information about whether the result provides and error or not. As this is depending on the TResult-type, the AResult-template has to be extended.

enum class EError 
{
    Ok,
    Error
};

std::ostream &operator<<(std::ostream &aStream, EError const &aError){ ... }

// If no type provided, will decay to result code only version
template <typename TData = void> 
class CResult
    : public AResult<EError, TData>
{
public:
    using AResult<EError, TData>::AResult;
    
    inline bool successful() const
    {
        return (EError::Ok == AResult<EError, TData>::result()); 
    }
};

If extended, the result type can be used as below:

CResult<> const isEven(uint32_t const &aArg)
{
    return ((0 == (aArg % 2)) ? EError::Ok : EError::Error);
}

CResult<uint32_t> const timesTwoIfEven(uint32_t const &aArg)
{
    if(isEven(aArg).successful())
    {
        return { EError::Ok, (2 * aArg) };
    } 
    else 
    {
        return { EError::Error };
    }
}

int main()
{
    CResult<uint32_t> const result0 = timesTwoIfEven(2);
    std::cout << "Input 2 -> " << result0.result() << ", " << result0.data() << "\n";
    CResult<uint32_t> const result1 = timesTwoIfEven(3);
    std::cout << "Input 3 -> " << result1.result() << ", " << result1.data() << "\n";
}

If the function yielding a CResult<TResult, TData> does not return a value, but only a result code, just use <>, which will instantiate the specialization of AResult<TResult, TData> with TData := void, yielding a compatible AResult<TResult>, but without unnecessary data consumption.

Conclusion

Using the AResult<TResult, TData>-construct permitted me to clean up my codebase enormously.

I was able to have a single signature style for all functions, remove result-data tuples and remove every single std::exception of my self written code.

This does not resolve exception handling, though. The Standard Libary could throw exceptions, as well as third party dependencies, if used.

But it allows me to avoid it, while maintaining a clean system of error handling in my code base.

Finally, find a fully working example here: https://wandbox.org/permlink/81kLJVGkiuoa2Cqi