Search code examples
c++boosterror-handlingc++17

Error handling with (boost::)outcome : use in constructor with composition and inheritance


In my environment I cannot use exceptions so I need an alternative solution for error-handling. Returning an int as the error code is not a good way in a new modern project because this interface prevents to return other data.

std::expected is not yet available; maybe there are some sample implementations but I need something already tested and robust.

I'm evaluating (boost::)outcome https://ned14.github.io/outcome/ and it seems to fit my needs: it has a clear interface and it should be very efficient if no auxiliary payloads are used.

The use case for generic class method is quite simple: https://ned14.github.io/outcome/tutorial/essential/result/

Regarding the usage with constructors, the author suggests a dual phase construction (https://ned14.github.io/outcome/tutorial/advanced/constructors/).

The tutorial does not talk about class composition and inheritance. The following example is the same as the tutorial.

class A {
protected: // use protected because of C class in the next example
    constexpr A() noexcept { /*...*/ }
public:
    ~A() noexcept { /*...*/ }

    A( A&& rhs ) noexcept { /*...*/ }

    A& operator=( A&& rhs ) noexcept 
    {
        this->~A();
        new(this) A( std::move( rhs ) );
        return *this;
    }

    // Remove copy ctor and assignment op
    A( const A& ) = delete;
    A& operator=( const A& ) = delete;

    /** Static member constructor */
    static result<A> A_ctor() noexcept 
    {
        // Phase 1 ctor
        A ret;
        // Phase 2 ctor
        if ( /*something goes wrong*/ ) return MyErrorCode::Error;
        return { std::move( ret ) };
    }

    void a_method() noexcept { /*...*/ }
};

template<> struct make<A>
{
    result<A> operator()() const noexcept
    {
        return A::A_ctor();
    }
};

Now consider the class B that contains the class A. Being A ctor protected, the following declaration is not valid:

class B {
    A a_;
    ...
};

Maybe the following could work:

class B {
    result<A> a_;
    constexpr B() noexcept : a_( make<A>{}() ) {}
public:
    static result<B> B_ctor() noexcept
    {
        // Phase 1 ctor
        B ret;
        // Phase 2 ctor
        if ( ret.value().a_.has_failure() ) return MyErrorCode::Error;
        if ( /*something else goes wrong*/ ) return MyErrorCode::AnotherError;
        return { std::move( ret ) };
    }

    // ...

    void b_method() noexcept
    {
        a_.value().a_method(); // <-- ugly!
        // ...
    }
};

but using result<A> as type for a_ is not very nice. It requires to use a_.value() everywhere in the code where a_ is used. Moreover if a_ is often used, efficiency could be reduced. Is there any other solution?

There's another dark point with derived classes.

class C : public A {
    constexpr C() noexcept : A() { /*...*/ }
public:
    // ...

    static result<C> C_ctor() noexcept
    {
        // Phase 1 ctor
        C ret;
        // Phase 2 ctor
        // How to reuse A ctor???
        // ...
        return { std::move( ret ) };
    }
};

In C_ctor I would like to construct the class starting from A_ctor to avoid code duplication, something like:

result<C> ret = C::A_ctor();

but there's no available conversion. Any idea to solve this point?


Solution

  • Being A ctor protected, the following declaration is not valid.

    You cannot indeed use non-accessible constructors, but move constructor is public, so you might write your X_ctor differently:

    B(A&& a) noexcept : a_(std::move(a)) {} // use public A(A&&)
    
    static result<B> B_ctor() noexcept
    {
        result<A> a = make<A>(); 
        if ( a.has_failure() ) return MyErrorCode::Error;
    
        // Phase 1 ctor
        B ret(std::move(a.value()));
    
        // Phase 2 ctor
        if ( /*something else goes wrong*/ ) return MyErrorCode::AnotherError;
        return { std::move( ret ) };
    }
    

    In C_ctor I would like to construct the class starting from A_ctor to avoid code duplication

    You might have init functions:

    result<bool> init() noexcept 
    {
        // ...
        if ( /*something goes wrong*/ ) return MyErrorCode::Error;
        return {true};
    }
    
    static result<A> A_ctor() noexcept 
    {
        // Phase 1 ctor
        A ret;
        // Phase 2 ctor
        result<bool> a_init = ret.init();
        if ( a_init.has_failure() ) return a_init.error();
        return { std::move( ret ) };
    }
    

    and

    result<bool> init() noexcept 
    {
        result<bool> a_init = A::init();
        if ( a_init.has_failure() ) return a_init.error();
    
        // ...
        if ( /*something goes wrong*/ ) return MyErrorCode::Error;
        return {true};
    }
    
    static result<C> C_ctor() noexcept
    {
        // Phase 1 ctor
        C ret;
    
        // Phase 2 ctor
        result<bool> c_init = ret.init();
        if ( c_init.has_failure() ) return c_init.error();
        return { std::move( ret ) };
    }