Category: C++

  • Having fun with Detection idiom and macros in C++

    Introduction

    The detection idiom is now well known in C++. However, some implementations necessitate a lot of boilerplate, and I do believe that macros may help here. So, let’s having fun with the detection idiom and macros in C++!

    The goal with detection idiom in C++

    The detection idiom is used to know at compile time if an expression is valid, or not. For example, it can be used to know if a class has a function, an attribute, an operator…

    Our objective will be to write something like

        constexpr auto additionnable = is_valid((x, y), x + y);
        
        static_assert(additionnable(a, b));
        static_assert(!additionnable(a, c));
        static_assert(additionnable(c, c));
        static_assert(!additionnable(c, a));

    It is easy to read. The is_valid function (actually, it will be a macro) just returns a helper function with 2 arguments that return true_type if the expression is possible or returns false_type otherwise.

    Basic detection idiom in C++

    To implement the idiom detection in C++, you may use polymorphic lambdas. If you chose this solution, you will need 3 things.

    1. The case where the expression is not possible
    2. The case where the expression is possible
    3. A wrapper to make things easier.

    The first case is really easy

    template<typename F>
    std::false_type is_valid_impl(...);

    It is a function template that takes a function as a first parameter and returns false.

    The second case is a bit more complicated.

    template<typename F, typename ...Xs>
    constexpr auto is_valid_impl(Xs&& ...xs) -> decltype(std::declval<F>()(FWD(xs)...), void(), std::true_type());

    The idea is simple, we test to call F with the given arguments, and we return true. The void() is used because the result of F(xs...) may returns something that implements operator,.

    The third case is easy, it is just a simple wrapper.

    template<typename F>
    constexpr auto is_valid_f(F) {
        return [](auto &&...xs) -> decltype(is_valid_impl<F>(FWD(xs)...)){ return {}; };
    };

    The usage is easy, but requires a lot of boilerplate …

        int a = 0;
        int b = 1;
        std::string c;
        
        constexpr auto additionnable = is_valid_f([](auto a, auto b) -> decltype(a + b){return a + b;});
            
        static_assert(additionnable(a, b));
        static_assert(!additionnable(a, c));
        static_assert(additionnable(c, c));
        static_assert(!additionnable(c, a));

    Making thing easier with simple macros

    A simple macro could simplify things.

    #define is_valid2(a, b, expr) is_valid_f([](auto a, auto b) -> decltype(expr) {return expr;})
    constexpr auto additionnable = is_valid2(x, y, x + y);

    It is now much easier to read, however, it requires you to make a is_valid macro for each number of arguments, and it is not a good thing at all…

    Going further with macros

    Basics

    The first thing you must understand is that recursive macros are theoretically impossible. Macros work by expansion. Each time you will expand a macro, the underlying called macros will be _painted blue_ and will not be called again. For example, if you use #define X X, when you will write X, it will not recurse until your death, it will just replace X by … X.

    However, you may use indirect recursion ! Let’s make a first try

    #define X() X_I()
    #define X_I() X()
    X()

    Do you think it will work? The answer is no. You expand X(), so you call X_I(). The call to X_I expands to X which is painted blue inside this expansion…

    What we must do is to defer the call to X and the call to X_I and force expansion several times :

    #define EMPTY()
    #define DEFER(...) __VA_ARGS__ EMPTY()
    #define EXPAND(...) __VA_ARGS__
    #define EVAL1(...) EXPAND(EXPAND(EXPAND(__VA_ARGS__)))
    #define EVAL2(...) EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
    #define EVAL3(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
    #define EVAL(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
    
    #define X() DEFER(X_I)()
    #define X_I() DEFER(X)()

    Now, we are ready. We must define an interface for our macro. I propose the following :

    #define is_valid(args, expr)

    With such an interface, you will not be able to pass a list of arguments directly, so you must wrap them. A usual way to wrap arguments with macro is a parenthesis, so you will write is_valid((a, b, c, d), expr).

    To deal with such list, the easiest way is to call another macro

    #define MACRO(a, b, c, d)
    #define is_valid(args, expr) MACRO args
    is_valid((a, b, c, d), expr);

    This expression will lead to MACRO(a, b, c, d)

    The complicate task

    What will be difficult here is to create a macro that takes some arguments in a list and transform each value into auto &&x,

    The goal here is to transform MACRO(a, b, c, d) into [](auto &&a, auto &&b, auto &&c, auto &&d);

    An interesting idea is to create a MAP macro that takes 2 arguments. The MACRO to apply and the arguments. The macro to apply here could be a MAKE_AUTO(x).

    #define MAKE_AUTO(x) auto &&x

    Going that way, it is easy to understand that MAKE_AUTO(a) expands to auto &&a.

    So, let’s design MAP. As I explained before, it will take 2 arguments, so we can lay:

     #define MAP(MACRO, args)

    This macro will work as follows. Take the first argument and apply the macro to it. When the tail is not empty, perform recursion on the tail. Here is my definition of MAP macro.

    #define MAP(MACRO, args)                                                       \
     #define MAP(MACRO, args)                                                       \
      MACRO(HEAD args)                                                             \
      WHEN(NOT(IS_EMPTY(TAIL args)))(DEFER_TWICE(MAP_I)(MACRO, TAIL args))
    
    #define MAP_I(MACRO, args) , DEFER_TWICE(MAP)(MACRO, args)

    So, here are the explanations. The beginning is easy, you just call the MACRO with the first argument. Here is the definition of HEAD and TAILS

    #define HEAD(x, ...) x
    #define TAIL(x, ...) (__VA_ARGS__)

    HEAD takes a list and returns the first element, TAIL returns the list of all elements but the first.

    The second line is where the black magic occurs. Here are some high-level explanations before showing the code.

    • WHEN(c)(expr) will expand to expr only if c == 1
    • NOT(x) will expands to 1 if x == 0 and to 0 if x == 1
    • IS_EMPTY(args) will expand to 1 if the list of args is empty or 0 otherwise
    • DEFER_TWICE is needed because WHEN already make one expansion.
    • MAP_I adds a , and call MAP again

    When you develop using macros, I advice you to use these 2 helpers:

    #define CAT_IMPL(x, y) x##y
    #define CAT(x, y) CAT_IMPL(x, y)
    
    #define STRINGIFY_IMPL(...) #__VA_ARGS__
    #define STRINGIFY(...) STRINGIFY_IMPL(__VA_ARGS__)

    The CAT macro will concat x and y, and STRINGIFY will transform arguments into a string. That may help you for debugging for example. The IMPL versions does not make expansion of arguments, but the not _IMPL versions do.

    #define X Y
    STRINGIFY(X) // "Y"
    STRINGIFY_IMPL(X) // "X"

    The first Macro we are going to define is NOT. It is easy to define

    #define NOT1 0
    #define NOT0 1
    #define NOT(x) CAT_IMPL(NOT, x)

    We use the CAT_IMPL and not the CAT version because x will be expanded here, so we do not need another level of expansion. For people coming from a functional world, it may remain pattern matching.

    The second macro we are going to define is DEFER_TWICE. The idea follows the DEFER macro:

    #define DEFER_TWICE(...) __VA_ARGS__ DEFER(EMPTY)()
    

    Let’s define the WHEN macro.

    #define WHEN0(...)
    #define WHEN1(...) __VA_ARGS__
    #define WHEN(c) CAT_IMPL(WHEN, c)

    It is easy, we do pattern matching to a function that does nothing when c is 0, and we expand the other arguments when c is 1.

    The most difficult macro to define is IS_EMPTY. I told you about an end sentinel. A simple end sentinel to use with macro is a parenthesizes. So, we just add () at the end of arguments. For example : (a, b, c, ()) represents a list that contain [a, b, c].

    IS_EMPTY(x) must return 1 only when x looks like (()). Here is my solution

    #define CHECK_IMPL(x, n, ...) n
    #define CHECK(...) CHECK_IMPL(__VA_ARGS__, 0)
    #define PROBE(...) ~, 1
    
    #define IS_EMPTY_IMPL(args) PROBE args
    #define IS_EMPTY(args) CHECK(IS_EMPTY_IMPL(HEAD args))

    Now that we have an almost working solution (I said almost because MAP will not work with an empty list, but it is not difficult to fix that), we can make the definition for our is_valid macro.

    #define is_valid(args, expr)                                                   \
      is_valid_f(                                                                  \
          [](EVAL(MAP(MAKE_AUTO, ADD_END_SENTINEL args))) -> decltype(expr) {      \
            return expr;                                                           \
          })

    The ADD_END_SENTINEL macro is easy. It just appends () to the list.

    #define ADD_END_SENTINEL(...) (__VA_ARGS__, ())

    And now, we can enjoy our new function !

    struct Cat {
      void meow();
    };
    
    struct Dog {
      void bark();
    };
    
    int main() {
      int a = 0;
      int b = 1;
      std::string c;
      Cat cat;
      Dog dog;
    
      constexpr auto additionnable1 = is_valid((... xs), ((... + xs)));
      constexpr auto additionnable2 = is_valid((x, y), x + y);
      constexpr auto has_meow = is_valid((x), x.meow());
    
      static_assert(additionnable1(a, b));
      static_assert(!additionnable1(a, c));
      static_assert(additionnable1(c, c));
      static_assert(!additionnable2(c, a));
      static_assert(has_meow(cat));
      static_assert(!has_meow(dog));
    }

    Conclusion

    I hope you liked this article and you learned something. If you have any questions, feel free to ask, I would be glad to answer.

    If you want to try it, here is the full implementation.

  • Thoughts about getters and setters in C++

    This article deals with getters and setters in C++. I am sorry, it is not about coroutine, but part 2 of thread pools will come in the following weeks.

    TL;DR: Getters and setters are bad for structure like objects.

    Introduction

    In this article, I will only give my opinion about them, I don’t want to offend anyone, I am just going to explain why, or why not, use getters and setters. I would be glad to have any debates in the comments part.

    Just to be clear about what I am talking about when I talk about getter, I talk about a function that just returns something, and when I talk about setter, I talk about a function that just modifies one internal value, not doing any verification or other computations.

    Getter performances

    Let’s say we have a simple structure with their usual getter and setters:

    class PersonGettersSetters {
      public:
        std::string getLastName() const { return m_lastName; }
        std::string getFirstName() const { return m_firstName; }
        int getAge() const {return m_age; }
        
        void setLastName(std::string lastName) { m_lastName = std::move(lastName); }
        void setFirstName(std::string firstName) { m_firstName = std::move(firstName); }
        void setAge(int age) {m_age = age; }
      private:
        int m_age = 26;
        std::string m_firstName = "Antoine";
        std::string m_lastName = "MORRIER";    
    };

    Let’s compare this version with the one without getters and setters.

    struct Person {
        int age = 26;
        std::string firstName = "Antoine";
        std::string lastName = "MORRIER";
    };

    It is incredibly shorter and less error_prone. We can not make the error to return the last name instead of the first name.

    Both codes are fully functional. We have a class Person with the first name, the last name, and an age. However, let’s say we want a function that returns the presentation of a person.

    std::string getPresentation(const PersonGettersSetters &person) {
      return "Hello, my name is " + person.getFirstName() + " " + person.getLastName() +
      " and I am " + std::to_string(person.getAge());
    }
    
    std::string getPresentation(const Person &person) {
      return "Hello, my name is " + person.firstName + " " + person.lastName + " and I am " + std::to_string(person.age);
    }
    Show the performance differences between computation using getters by value and public access.

    The version without getters performs this task 30% quicker than the version with getters. But why? It is because of the return by value of the getters functions. Returning by value makes a copy that results in poorer performance. Let’s compare the performance between person.getFirstName(); and person.firstName.

    Show the performance differences between getters by value and public access.

    As you can see, accessing directly the first name instead of a getter is equivalent to a noop.

    Getter by const reference

    However, it is possible to not return by value but return by reference instead. Going that way, we will have the same performance as without getters. The new code will look like that

    class PersonGettersSetters {
      public:
        const std::string &getLastName() const { return m_lastName; }
        const std::string &getFirstName() const { return m_firstName; }
        int getAge() const {return m_age; }
        
        void setLastName(std::string lastName) { m_lastName = std::move(lastName); }
        void setFirstName(std::string firstName) { m_firstName = std::move(firstName); }
        void setAge(int age) {m_age = age; }
      private:
        int m_age = 26;
        std::string m_firstName = "Antoine";
        std::string m_lastName = "MORRIER";    
    };
    

    Since we get the same performance as before, are we done? To answer this question, you can try to execute this code.

    PersonGettersSetters make() {
        return {};   
    }
    
    int main() {
        auto &x = make().getLastName();
         
        std::cout << x << std::endl;
        
        for(auto x : make().getLastName()) {
            std::cout << x << ",";   
        }
    }

    You may see some weird characters wrote in the console. But why? What happened when you do make().getLastName()?

    1. You create a Person
    2. You get a reference to the last name
    3. You delete Person

    You have a dangling reference! It can lead to a crash (in the best case) or something worst than what can be found in a horror movie.

    To deal with such a thing, we must introduce reference qualified functions.

    class PersonGettersSetters {
      public:
        const std::string &getLastName() const & { return m_lastName; }
        const std::string &getFirstName() const & { return m_firstName; }
        
        std::string getLastName() && { return std::move(m_lastName); }
        std::string getFirstName() && { return std::move(m_firstName); }
        
        int getAge() const {return m_age; }
        
        void setLastName(std::string lastName) { m_lastName = std::move(lastName); }
        void setFirstName(std::string firstName) { m_firstName = std::move(firstName); }
        void setAge(int age) {m_age = age; }
        
      private:
        int m_age = 26;
        std::string m_firstName = "Antoine";
        std::string m_lastName = "MORRIER";    
    };

    Here is the new solution that works everywhere. You need 2 getters. One for lvalue and one for rvalue (both xvalue and prvalue).

    Setter issues

    There is not a lot to say in this section. If you want to achieve the best performances, you must write a setter that takes a lvalue and one that takes a rvalue. However, it is generally fine to just have a setter that takes a value that will be moved. Nevertheless, you have to pay the price of an extra move. However, that way, you cannot just make a little enhancement. You must replace the whole variable. If you just wanted to replace one A by a D in a name, it will not be possible by using setters. However, using direct access makes it possible.

    What about immutable variables?

    Ones will tell you to just make the member attribute as const. However, I am not ok with this solution. Indeed, making it const will prevent the move semantic and will lead to unnecessary copy.

    I have no magic solution to propose you right now. Nevertheless, we can write a wrapper which we can named immutable<T>. This wrapper must be able to be :

    1. Constructible
    2. Since it is immutable, it must not be assignable
    3. It can be copy constructible or move constructible
    4. It must be convertible to const T& when being a lvalue
    5. It must be convertible to T when being a rvalue
    6. It must be used like other wrapper through operator* or operator->.
    7. It must be easy to get the address of the underlying object.

    Here is a little implementation

    #define FWD(x) ::std::forward<decltype(x)>(x)
    
    template <typename T>
    struct AsPointer {
        using underlying_type = T;
        AsPointer(T &&v) noexcept : v{std::move(v)} {}
        T &operator*() noexcept { return v; }
        T *operator->() noexcept { return std::addressof(v); }
        T v;
    };
    
    template <typename T>
    struct AsPointer<T &> {
        using underlying_type = T &;
        AsPointer(T &v) noexcept : v{std::addressof(v)} {}
        T &operator*() noexcept { return *v; }
        T *operator->() noexcept { return v; }
        T *v;
    };
    
    template<typename T>
    class immutable_t {
      public:
        template <typename _T>
        immutable_t(_T &&t) noexcept : m_object{FWD(t)} {}
    
        template <typename _T>
        immutable_t &operator=(_T &&) = delete;
    
        operator const T &() const &noexcept { return m_object; }
        const T &operator*() const &noexcept { return m_object; }
        AsPointer<const T &> operator->() const &noexcept { return m_object; }
    
        operator T() &&noexcept { return std::move(m_object); }
        T operator*() &&noexcept { return std::move(m_object); }
        AsPointer<T> operator->() &&noexcept { return std::move(m_object); }
    
        T *operator&() &&noexcept = delete;
        const T *operator&() const &noexcept { return std::addressof(m_object); }
    
        friend auto operator==(const immutable_t &a, const immutable_t &b) noexcept { return *a == *b; }
    
        friend auto operator<(const immutable_t &a, const immutable_t &b) noexcept { return *a < *b; }
    
      private:
        T m_object;
    };

    So, for an immutable Person, you can just write:

    struct ImmutablePerson {
        immutable_t<int> age = 26;
        immutable_t<std::string> firstName = "Antoine";
        immutable_t<std::string> lastName = "MORRIER";
    };

    Conclusion

    I would not say that getters and setters are bad. However, when you do not need to do anything else in your getter and your setter, achieving the best performance, safety and flexibility lead to writing:

    • 3 getters (or even 4): const lvalue, rvalue, const rvalue, and if you want, non-const lvalue (even if sounds really weird since it is easier to just use direct access)
    • 1 setter (or 2 if you want to have the maximum performance)

    It is a lot of boilerplate for almost anything.

    Some people will tell you that getters and setters enforce encapsulation, but they don’t. Encapsulation is not just making attributes private. It is about hiding things from users, and for structure-like objects, you rarely want to hide anything.

    My advice is: when you have a structure like objects, just don’t use getter and setters and go with public / direct access. To be simple, if your setter does not enforce any invariant, you don’t need a private attribute.

    PS: For people who use libraries using shallow copy, the performance impact is less important. However, you still need to write 2 functions instead of 0. Don’t forget, the less code you write, the less bugged it will be, easier to maintain, and easier to read.

    And you ? Do you use getters and setters? And why?

  • C++ error handling, let’s abuse the co_await operator

    Introduction

    Sometimes (very rarely :-p), errors may happen. It can be due to misuse from the user, it can be due to a system error, a corrupted, or a missing file. Errors can pop from everywhere. There are several ways to handle errors. In this article, we are going to see how coroutine may be used for error handling, so let’s abuse the co_await operator

    Old ways to handle errors

    One of the first ways to handle errors used (AFAIK) was the error code return. The idea is simple, you return OK when the function executes properly, else you return an error code, like OUT_OF_RANGE, FILE_NOT_EXIST

    You ended with code like:

    enum class error_code { OK, ERROR };
    
    error_code g(bool success, int &value) {
      if (!success)
        return error_code::ERROR;
      value = 5;
      return error_code::OK;
    }
    
    error_code f(bool success, int &value) {
      if (g(success, value) == error_code::OK) {
        value *= 2;
        return error_code::OK;
      }
      return error_code::ERROR;
    }
    
    int main() {
      int value;
    
      if (f(false, value) == error_code::ERROR) {
        std::cout << "error!" << std::endl;
      }
    
      if (f(true, value) == error_code::OK) {
        std::cout << value << std::endl;
      }
    }
    

    As expected, the code will write error! and 10.

    The advantage of this way is that it is really explicit, you know which function can fail. A disadvantage is that there is no propagation of errors. You must treat error in the same place that you treat the correct path. Another problem is it can lead to severe problems if the client of your function does not check if it fails or not. Believe me, programmers are somewhat lazy, and they may forget to check the return value and they will process the result like if it was correct. For example, let’s imagine you developed an initialize() function, it returns fails, but you use the object not initialized. it will, sooner or later, lead to a severe failure.

    Another way to process errors is the exception. When you detect an error, you throw the error, and when you want to process the error, you catch it. It solves the problems both of error propagation and the use of non initialized objects we saw prior. The code will look like that:

    #include <stdexcept>
    
    int g(bool success) {
      if (!success)
        throw std::runtime_error("Error !");
      return 5;
    }
    
    int f(bool success) { return g(success) * 2; }
    
    int main() {
      try {
        std::cout << f(true) << std::endl;
        std::cout << f(false) << std::endl;
      } catch (const std::exception &exception) {
        std::cout << exception.what() << std::endl;
      }
    }

    The code is shorter, and the error handling is done when you need it. However, it is difficult to know if the function f can fail if we don’t have the code of g(). We can doubt because f is not noexcept, but that’s all, and it does not give us so much information about the possible error.

    A modern way for error-handling thanks to ADT

    There is a proposal for an expected object for C++23. Basically, std::excpected is a template object which takes 2 arguments.

    1. The result type
    2. The error type

    It is a sum-type, so it can contain either the result or the error. However, the std::expected way to handle errors will be a bit like the error code, you don’t get automatic propagation. However, you may use the functional approaches that simulate the error propagation:

    expected<int, std::string> g(bool success) {
        if(success)
            return 5;
        return "Error!";
    }
    
    expected<int, std::string> f(bool success) {
        return g(success).map([](auto x){return x * 2;});   
    }
    
    int main() {
        auto a = f(false);
        auto b = f(true);
        
        if(a.is_error())
            std::cout << a.error() << std::endl;
        if(b.is_result())
            std::cout << b.result() << std::endl;
    }

    The map function will be executed only if g(success) is not an error, if it is an error, the error will be propagated to the caller.

    All the lambda thing is very good, works perfectly, is pretty_fast and readable. However, in some cases, it can become cumbersome.

    In the rust language programming, we would write something like:

    fn f(success: bool) -> Result<i32, String> {
        let value = g(success)?;
        value * 2
    }

    Note the presence of the operator ?. It means, if g(success) succeed, so continue the execution, else, stops the execution and propagates the error to the caller.

    Did this story of stopping and continue the execution reminds you something?

    Let’s abuse the co_await operator !

    The objective will be to be able to write something like:

    Expected<int, std::string> g(bool success) {
      if (!success)
        return "Error !!";
      return 5;
    }
    
    Expected<int, std::string> f(bool success) {
      int result = co_await g(success);
      co_return result * 2;
    }

    You can even imagine a macro try or TRY to make things even better :p. But be careful if you are using exceptions :).

    Let’s design a simple Expected class.

    template <typename Result, typename Error> class Expected {
    public:
      template <typename T> Expected(T t) : m_value{std::move(t)} {}
    
      bool is_result() const { return m_value.index() == 1; }
      Result result() const { return *std::get_if<1>(&m_value); }
    
      bool is_error() const { return m_value.index() == 2; }
      Error error() const { return *std::get_if<2>(&m_value); }
    
      template <typename T> Expected &operator=(T t) {
        m_value = std::move(t);
        return *this;
      }
    
    private:
      std::variant<std::monostate, Result, Error> m_value;
    };

    I didn’t use reference qualified member for the sake of simplicity. However, in a production code, to have the best performance, you must use them to avoid useless copy etc.

    I use a std::variant with std::monostate because we are going to need it later. So, basically, we have a class that represents either a result or an error. You have a function to ask which value is carried by the Expected and you have a function to retrieve the result or the error.

    As we said before, Expected is meant to be used with coroutines. It must have a nested promise_type

    The promise_type

    We remind that the promise_type must have 5 member functions.

    1. get_return_object() which will return an expected
    2. return_value() / return_void() which will handle the co_return operator.
    3. initial_suspend and final_suspend that handle the beginning and the end of the coroutine
    4. unhandled_exception that handles unhandled exceptions.

    In our example, unhandled_exception will do nothing for the sake of simplicity. initial_suspend and final_suspend will be of std::suspend_never because when we launch the function, we want it to not be paused, and when we exit the function, we expect everything to be cleared properly.

    Let’s talk about the get_return_object() and return_value(). We are going to begin with return_value(). Its prototype will be something like void return_value(Expected result). We can write different overloads for Result and Error and their reference qualified friends, but for the sake of simplicity, again, I chose to have only the Expected overload :-).

    We must do something with this result, we must set the current expected with this value. To do that, I decided to use a pointer on the current Expected instance.

    For the get_return_object function, things are not that easy. You must be able to construct an expected without an error or a result. Moreover, you must initialize the pointer to the expected in the promise_type.

    Then, I added a private constructor to the Expected object.

    private:
      Expected(promise_type &promise) noexcept { promise.expected = this; }
    

    The promise_type is as we described prior.

      struct promise_type {
        Expected *expected;
        Expected get_return_object() { return {*this}; }
        void return_value(Expected result) { *expected = std::move(result); }
    
        std::suspend_never initial_suspend() const noexcept { return {}; }
        std::suspend_never final_suspend() const noexcept { return {}; }
    
        void unhandled_exception() noexcept {}
      };

    However, be careful with your get_return_object function. Here it works because of guaranteed copy elision. If there was no elision, you will get a segmentation fault(in the best case) because the Expected address will not be the same 🙂

    Our Expected object can be co_returned from a coroutine, but it can not be co_awaited . So, let’s abuse the co_await operator !

    Awaiter

    To make our Expected class awaitable, we must define an Awaiter class.

    As a reminder, an awaiter must have three functions.

    1. await_ready: which returns a bool to know if we can continue the execution, or suspend it.
    2. await_resume: which returns the type wanted from co_await x.
    3. await_suspend: which is called when a coroutine is suspended.

    The await_ready is really simple. If the expected is an error, we suspend, else, we continue. The await_resume function just returns the result. The await_suspend function is the most complicated! It is called when we have an error, it must give the error to the expected returned by get_current_object. Moreover, it must destroys the current coroutine.

    Hence, here is the code for Awaiter class and the operator co_await:

      struct Awaiter {
        Expected expected;
    
        bool await_ready() { return expected.is_result(); }
        Result await_resume() { return expected.result(); }
        void await_suspend(std::coroutine_handle<promise_type> handle) {
          *handle.promise().expected = expected;
          handle.destroy();
        }
      };
    
      Awaiter operator co_await() { return Awaiter{*this}; }

    Again, I do not manage reference qualified methods. You must do it in production code. Here is the full code if you want it.

    Performance ?

    It is purely one hypothesis, but I do believe that in the future, the compiler could be optimized out of this kind of code. Indeed, from cpp-reference, we can read :

    • The lifetime of the coroutine state is strictly nested within the lifetime of the caller, and
    • the size of coroutine frame is known at the call site

    The first is obvious, the second I think yes, but I am not sure, that is why it is one hypothesis.

    Thanks for reading :).