Giter Site home page Giter Site logo

zpp_throwing's Introduction

zpp::throwing

.github/workflows/actions.yml Build Status

Abstract

C++ exceptions and RTTI are often not enabled in many environments, require outstanding library and ABI specific support, and sometimes introduce unwanted cost (i.e exceptions failure path is often considered very slow, RTTI often grows binary size and adds type names to the program binaries which may hurt confidentiality). Most implementations of C++ exceptions also use the RTTI implementation, enlarging the price to be paid for using exceptions.

Exceptions are however very convenient, particularly the automatic error propagation which helps makes code clearer and easier to read, write and maintain. In the journey to try and get a working alternative to standard C++ exceptions for error handling, there are many modern return value based facilities out there, and even macro based utilities to do the "automatic" propagation. There are also known papers and proposals to improve/introduce new and better form of exceptions in C++ which I hope will become part of future standard.

So far there is no solution existing today that I know of, that is "close enough" to the experience of using plain C++ exceptions, that I consider clean, meaning, with automatic and invisible error propagation, and without polluting the code with macros.

Motivation

With this library I am trying to provide a coroutine based implementation that resembles C++ exceptions as close as I could get. I made sure that it works also when compiling without exceptions and RTTI (i.e -fno-exceptions -fno-rtti).

Contents

Introduction / Hello World

Let's take a look at the following code:

zpp::throwing<int> foo(bool success)
{
    if (!success) {
        // Throws an exception.
        co_yield std::runtime_error("My runtime error");
    }

    // Returns a value.
    co_return 1337;
}

Foo returns a zpp::throwing<int> which means it can throw an exception in our implementation, but on success it returns an int.

int main()
{
    return zpp::try_catch([]() -> zpp::throwing<int> {
        std::cout << "Hello World\n";
        std::cout << co_await foo(false) << '\n';;
        co_return 0;
    }, [&](const std::exception & error) {
        std::cout << "std exception caught: " << error.what() << '\n';
        return 1;
    }, [&]() {
        std::cout << "Unknown exception\n";
        return 1;
    });
}

The zpp::try_catch function attempts to execute the function object that is sent to it, and when an exception is thrown, one of the following function objects that receives an exception from the appropriate type will get called, similar to a C++ catch block. The last function object can optionally receive no parameters and as such functions as catch (...) to catch all exceptions.

In this example foo was called with false, and hence it throws the std::runtime_error exception which is caught at the first lambda function sent as catch block. This lambda then returns 1 which eventually being returned from main.

Throwing Exceptions From Within Catch Blocks

Like in normal catch block, it is possible to throw from the catching lambdas:

zpp::throwing<std::string> bar(bool success)
{
    return zpp::try_catch([&]() -> zpp::throwing<std::string> {
        auto foo_result = co_await foo(success);
        std::cout << "Foo succeeded" << foo_result << '\n';
        co_return "foo succeeded";
    }, [&](const std::runtime_error & error) -> zpp::throwing<std::string> {
        std::cout << "Runtime error caught: " << error.what() << '\n';
        co_yield std::runtime_error("Foo really failed");
    });
}

Note that the lambda function sent as a catch block now returns a zpp::throwing<std::string> which allows it to throw exceptions using co_return.

It is even possible to rethrow the caught exception using zpp::rethrow:

zpp::throwing<std::string> bar(bool success)
{
    return zpp::try_catch([&]() -> zpp::throwing<std::string> {
        auto foo_result = co_await foo(success);
        std::cout << "Foo succeeded" << foo_result << '\n';
        co_return "foo succeeded";
    }, [&](const std::runtime_error & error) -> zpp::throwing<std::string> {
        cout << "Runtime error caught: " << error.what() << '\n';
        co_yield zpp::rethrow;
    });
}

You may observe that once we change bar() to catch std::logic_error instead, the exception is actually caught by the main() function below as an std::exception:

zpp::throwing<std::string> bar(bool success)
{
    return zpp::try_catch([&]() -> zpp::throwing<std::string> {
        auto foo_result = co_await foo(success);
        std::cout << "Foo succeeded" << foo_result << '\n';
        co_return "foo succeeded";
    }, [&](const std::logic_error & error) -> zpp::throwing<std::string> {
        std::cout << "Logic error caught: " << error.what() << '\n';
        co_yield std::runtime_error("Foo really failed");
    });
}

int main()
{
    return zpp::try_catch([]() -> zpp::throwing<int> {
        std::cout << "Hello World\n";
        std::cout << co_await bar(false) << '\n';;
        co_return 0;
    }, [&](const std::exception & error) {
        std::cout << "std exception caught: " << error.what() << '\n';
        return 1;
    }, [&]() {
        std::cout << "Unknown exception\n";
        return 1;
    });
}

Registering Custom Exception Types

Since at the time of writing, there is no way in C++ to iterate base classes of a given type, manual registration of exception types is required. The following example shows how to do it:

struct my_custom_exception { virtual ~my_custom_exception() = default; };
struct my_custom_derived_exception : my_custom_exception {};

template <>
struct zpp::define_exception<my_custom_exception>
{
    using type = zpp::define_exception_bases<>;
};

template <>
struct zpp::define_exception<my_custom_derived_exception>
{
    using type = zpp::define_exception_bases<my_custom_exception>;
};

And then throw it like so:

zpp::throwing<int> foo(bool success)
{
    if (!success) {
        // Throws an exception.
        co_yield my_custom_derived_exception();
    }

    // Returns a value.
    co_return 1337;
}

Throwing Values (Inspired by P0709)

int main()
{
    zpp::try_catch([]() -> zpp::throwing<void> {
        // Throws an exception.
        co_yield std::errc::invalid_argument;
    }, [](zpp::error error) {
        std::cout << "Error: " << error.code() <<
            " [" << error.domain().name() << "]: " << error.message() << '\n';
    }, []() {
        /* catch all */
    });
}

In the main function above an error value is thrown, from a predefined error domain of std::errc.

In order to define your own error domains, the following example is provided:

enum class my_error
{
    success = 0,
    operation_not_permitted = 1,
    general_failure = 2,
};

template <>
inline constexpr auto zpp::err_domain<my_error> = zpp::make_error_domain(
        "my_error", my_error::success, [](auto code) constexpr->std::string_view {
    switch (code) {
    case my_error::operation_not_permitted:
        return "Operation not permitted.";
    case my_error::general_failure:
        return "General failure.";
    default:
        return "Unspecified error.";
    }
});

You can even catch the error enumeration directly, like so:

int main()
{
    zpp::try_catch([]() -> zpp::throwing<void> {
        // Throws an exception.
        co_return std::errc::invalid_argument;
    }, [](std::errc error) {
        switch(error) {
        case std::errc::invalid_argument: {
            std::cout << "Caught the invalid argument directly by enumeration type\n"
                << "And here is the message: " << zpp::error(error).message() << '\n';
            break;
        }
        default: {
            zpp::error my_error = error;
            std::cout << "Error: " << my_error.code() <<
                " [" << my_error.domain().name() << "]: " << my_error.message() << '\n';
        }
        }
    }, []() {
        /* catch all */
    });
}

Throwing Exceptions with co_yield vs co_return

You may throw also with co_return. The library will understand whether you are actually returning a value or throwing, by the type of the return expression. Theoretically co_return should generate better code because it does not add a suspend point but it is highly optimize-able, and the library actually takes care of destroying the coroutine on the first suspend.

-Example:

zpp::throwing<int> foo(bool success)
{
    if (!success) {
        // Throws an exception, with `co_return`.
        co_return std::runtime_error("My runtime error");
    }

    // ...

    if (!success) {
        // Throwing values with `co_return` is also possible.
        co_return std::errc::invalid_argument;
    }

    // Returns a value.
    co_return 1337;
}

Leaf Functions May Just Use Return

Because being a coroutine is an implementation detail, if you don't call any other throwing function, it is possible to just stay a normal function rather than become a coroutine, so throwing or returning a value can simply be achieved by a simple return.

zpp::throwing<int> foo(bool success)
{
    if (!success) {
        // Throws an exception.
        return std::runtime_error("My runtime error");
    }

    // Returns a value.
    return 1337;
}

Fully-Working Example

As a final example, here is a full program to play with:

#include "zpp_throwing.h"
#include <string>
#include <iostream>

zpp::throwing<int> foo(bool success)
{
    if (!success) {
        // Throws an exception.
        co_yield std::runtime_error("My runtime error");
    }

    // Returns a value.
    co_return 1337;
}

zpp::throwing<std::string> bar(bool success)
{
    return zpp::try_catch([&]() -> zpp::throwing<std::string> {
        auto foo_result = co_await foo(success);
        std::cout << "Foo succeeded" << foo_result << '\n';
        co_return "foo succeeded";
    }, [&](const std::runtime_error & error) -> zpp::throwing<std::string> {
        std::cout << "Runtime error caught: " << error.what() << '\n';
        co_return "foo failed";
    });
}

zpp::throwing<std::string> foobar()
{
    auto result = co_await bar(false);
    if (result.find("foo succeeded") == std::string::npos) {
        co_yield std::runtime_error("bar() apparently succeeded even though foo() failed");
    }

    co_return result;
}

int main()
{
    return zpp::try_catch([]() -> zpp::throwing<int> {
        std::cout << "Hello World\n";
        std::cout << co_await foobar() << '\n';;
        co_return 0;
    }, [&](const std::exception & error) {
        std::cout << "std exception caught: " << error.what() << '\n';
        return 1;
    }, [&]() {
        std::cout << "Unknown exception\n";
        return 1;
    });
}

Compiling and Running The Tests

Execute ./zpp.mk -j in the test folder. You can find the output in the out directory.

Please make sure that clang++ points to clang++-12 or above. For more info about this build system see here.

Limitations / Caveats

  1. The code currently assumes that no exceptions can ever be thrown and as such it is not recommended to use it in a project where exceptions are enabled.
  2. Because coroutines cannot work with constructors, it means that an exception cannot propagate from constructors natively, and it needs to be worked around, through a parameter to the constructor or other means such as factory functions.
  3. The code has gone only through very minimal testing, with recent clang compiler.
  4. The code requires C++20 and above.
  5. You must catch every dynamic exception that you throw, otherwise a memory leak of the exception object will occur, this is to optimize the non-trivial destruction that happens when propagating the exception.
  6. Assumes allocators are stateless, don't require size on deallocation, and return max aligned storage,
  • for simplicity - may change in the future.

Final Word

Please submit any feedback you can via issues, I would gladly accept any suggestions to improve the usage experience, performance, reference to other similar implementations, and of course bug reports.

zpp_throwing's People

Contributors

eyalz800 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

zpp_throwing's Issues

ASAN failure in destruction.with_exception with libc++-18

Hello again,

So, it seems we're allocating memory for an exception as throwing<void, void> but trying to free it as throwing<void, std::allocator<?>>?

I see we will be using std::allocator_traits<void> anyhow for promise allocation and how the exception is allocated by operator new, but don't see how exception_object_delete<void> does anything else than delete here.

Please see log below:

~/tinker/zpp_throwing-upstream/test  ➦ 5576687  ./out/debug/default/output --gtest_filter=destruction.with_exception --gtest_color=no
Running main() from src/gtest/src/gtest_main.cc
Note: Google Test filter = destruction.with_exception
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from destruction
[ RUN      ] destruction.with_exception
=================================================================
==7196==ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new vs free) on 0x504000002490
    #0 0x55cdead86ba6 in free (/home/prak/tinker/zpp_throwing-upstream/test/out/debug/default/output+0xe8ba6) (BuildId: 17a2985e6b792e85800559727365fceef932d013)
    #1 0x7f1f24db2134 in std::range_error::~range_error() (/lib/x86_64-linux-gnu/libc++abi.so.1+0x25134) (BuildId: 4a8d565301c3486aed798c5488475b519bc0a363)
    #2 0x55cdeadcbc66 in auto zpp::exit_condition<void, void>::exit_with_exception<std::runtime_error>(std::runtime_error&&)::exception_holder::~exception_holder() /home/prak/tinker/zpp_throwing-upstream/test/./include/../../zpp_throwing.h:787:50
    #3 0x55cdeadcbc66 in auto zpp::exit_condition<void, void>::exit_with_exception<std::runtime_error>(std::runtime_error&&)::exception_holder::~exception_holder() /home/prak/tinker/zpp_throwing-upstream/test/./include/../../zpp_throwing.h:787:50
    #4 0x55cdeadc5e16 in zpp::exception_object_delete<void>::operator()(zpp::exception_object*) /home/prak/tinker/zpp_throwing-upstream/test/./include/../../zpp_throwing.h:358:13
    #5 0x55cdeadc5e16 in std::__1::unique_ptr<zpp::exception_object, zpp::exception_object_delete<void>>::reset[abi:v180000](zpp::exception_object*) /usr/lib/llvm-18/bin/../include/c++/v1/__memory/unique_ptr.h:300:7
    #6 0x55cdeadc5e16 in std::__1::unique_ptr<zpp::exception_object, zpp::exception_object_delete<void>>::~unique_ptr[abi:v180000]() /usr/lib/llvm-18/bin/../include/c++/v1/__memory/unique_ptr.h:266:75
    #7 0x55cdeadc5e16 in void zpp::throwing<void, void>::catch_exception_object<destruction_with_exception_Test::TestBody()::$_0, void, false>(zpp::dynamic_object const&, destruction_with_exception_Test::TestBody()::$_0&&) /home/prak/tinker/zpp_throwing-upstream/test/./include/../../zpp_throwing.h:1469:17
    #8 0x55cdeadc5e16 in void zpp::throwing<void, void>::catches<destruction_with_exception_Test::TestBody()::$_0>(destruction_with_exception_Test::TestBody()::$_0&&) /home/prak/tinker/zpp_throwing-upstream/test/./include/../../zpp_throwing.h:1594:20
    #9 0x55cdeadc5e16 in decltype(auto) zpp::try_catch<destruction_with_exception_Test::TestBody()::$_1, destruction_with_exception_Test::TestBody()::$_0>(destruction_with_exception_Test::TestBody()::$_1&&, destruction_with_exception_Test::TestBody()::$_0&&) /home/prak/tinker/zpp_throwing-upstream/test/./include/../../zpp_throwing.h:1642:54
    #10 0x55cdeadc5e16 in destruction_with_exception_Test::TestBody() /home/prak/tinker/zpp_throwing-upstream/test/src/destruction.cpp:72:12
    #11 0x55cdeae2ecfd in void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest.cc
    #12 0x55cdeae2ecfd in testing::Test::Run() /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest.cc:2706:5
    #13 0x55cdeae315ad in testing::TestInfo::Run() /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest.cc:2885:11
    #14 0x55cdeae330a4 in testing::TestSuite::Run() /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest.cc:3044:30
    #15 0x55cdeae680f8 in testing::internal::UnitTestImpl::RunAllTests() /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest.cc:5903:44
    #16 0x55cdeae672fd in bool testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*) /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest.cc
    #17 0x55cdeae672fd in testing::UnitTest::Run() /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest.cc:5470:10
    #18 0x55cdeae097e7 in RUN_ALL_TESTS() /home/prak/tinker/zpp_throwing-upstream/test/./include/gtest/gtest.h:2492:46
    #19 0x55cdeae097e7 in main /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest_main.cc:52:10
    #20 0x7f1f24a29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #21 0x7f1f24a29e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #22 0x55cdeaceb474 in _start (/home/prak/tinker/zpp_throwing-upstream/test/out/debug/default/output+0x4d474) (BuildId: 17a2985e6b792e85800559727365fceef932d013)

0x504000002490 is located 0 bytes inside of 42-byte region [0x504000002490,0x5040000024ba)
allocated by thread T0 here:
    #0 0x55cdeadc397d in operator new(unsigned long) (/home/prak/tinker/zpp_throwing-upstream/test/out/debug/default/output+0x12597d) (BuildId: 17a2985e6b792e85800559727365fceef932d013)
    #1 0x7f1f24e1820f in std::runtime_error::runtime_error(char const*) (/lib/x86_64-linux-gnu/libc++.so.1+0x5420f) (BuildId: 05f9ca9f6c44d20c9226bd7c33d1945445670c76)
    #2 0x55cdeae2ecfd in void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest.cc
    #3 0x55cdeae2ecfd in testing::Test::Run() /home/prak/tinker/zpp_throwing-upstream/test/src/gtest/src/gtest.cc:2706:5

SUMMARY: AddressSanitizer: alloc-dealloc-mismatch (/home/prak/tinker/zpp_throwing-upstream/test/out/debug/default/output+0xe8ba6) (BuildId: 17a2985e6b792e85800559727365fceef932d013) in free
==7196==HINT: if you don't care about these errors you may set ASAN_OPTIONS=alloc_dealloc_mismatch=0
==7196==ABORTING

Does zpp_throwing support Visual Studio?

Hello.
I am creating conan packages of zpp_bits, zpp_throwing.
While I have confirmed that zpp_bits can be built with Visual Studio 2022, zpp_throwing is giving me compile error even with Visual Studio 2022.

Is there any plan to support zpp_throwing in Visual Studio as well as zpp_bits?
If there is no plan to support Visual Studio, I will drop support Visual Studio on zpp_throwing conan package.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.