March 25th, 2021

Creating a task completion source for a C++ coroutine: Producing nothing

Last time, we created a result_holder that can hold a reference, and we solved it by using a wrapper. But there’s another type that we can’t put in a result_holder, not even with the help of a wrapper. That type is void.

    struct wrapper
    {
        void value; // not allowed
    };

This doesn’t work because you cannot have an object of type void. You might nevertheless want to have a result_holder that “holds” a void, because that is basically an event: The result is the fact that something happened.

There are a few ways to work around this problem. One is to redirect void to some other type like bool, and just ignore the bool value. This is the approach that is often used in C# code: A task completion event of void is just a task completion event of bool where the bool is ignored.

But in C++, we have partial specialization, so we can get all fancy-like.

    template<typename T>
    struct wrapper
    {
        T value;
        T get_value() { return static_cast<T>(value); }
    };

    template<>
    struct wrapper<void>
    {
        void get_value() { }
    };

In the case of void, we use an empty class. This avoids the trouble of having to initialize a dummy bool member, and it opens the door to empty base class optimization, although we won’t take advantage of EBO here. We then add get_value methods to extract the value in a uniform way:

  • For void it returns nothing.
  • For references, it returns the reference.
  • For values, it returns a copy of the object.

(Recall that this is intended for an object that can be awaited multiple times, so the underlying object needs to be copyable so that each client that does a co_await gets its own copy.)

Now we can revise our code that sets the result so it knows the special way of setting nothing.

    template<typename Dummy = void>
    std::enable_if_t<std::is_same_v<T, void>, Dummy>
        set_result(node_list& list)
    {
        if (!ready.load(std::memory_order_relaxed)) {
            new (std::addressof(result.wrap)) wrapper{ };
            ready.store(true, std::memory_order_release);
            this->resume_all(list);
        }
    }
    template<typename Dummy = void>
    std::enable_if_t<!std::is_same_v<T, void>, Dummy>
        set_result(node_list& list, T v)
    {
        if (!ready.load(std::memory_order_relaxed)) {
            new (std::addressof(result.wrap))
                wrapper{ std::forward<T>(v) };
            ready.store(true, std::memory_order_release);
            this->resume_all(list);
        }
    }
    ...
};

template<typename T>
struct result_holder
    : async_helpers::awaitable_sync_object<
        result_holder_state<T>>
{
    ...

    template<typename Dummy = void>
    std::enable_if_t<std::is_same_v<T, void>, Dummy>
        set_result() const noexcept
    {
        this->action_impl(&state::set_result);
    }
    template<typename Dummy = void>
    std::enable_if_t<!std::is_same_v<T, void>, Dummy>
        set_result(T result) const noexcept
    {
        this->action_impl(&state::set_result,
            std::forward<T>(result));
    }
};

Getting the value back out is simpler thanks to our get_value helper.

    T get_result()
    {
        return result.wrap.get_value();
    }
};

Okay, so now we know how to deal with a result of nothing. But how do you report the failure to produce a result at all? We’ll look at that next time.

Bonus chatter: While we’re at it, we may as well put [[no_unique_address]] on the T value, in case T is an empty class.

    template<typename T>
    struct wrapper
    {
        [[no_unique_address]] T value;
        T get_value() { return static_cast<T>(value); }
    };
Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • Dwayne Robinson

    How I wish C++ just treated void as a regular type in many cases, at least for single instance occurrences (not bringing up the issue about what it means to have an array of void :b), without the need for specializations of something that would otherwise just work. Matt Calabrese had an interesting paper on that (p0146r1).

    • 紅樓鍮

      Raymond once covered what in C++ are dubbed “abominable functions”. Search for it at your own mental risk.

  • Alexis Ryan

    This is the approach that is often used in C# code

    I have done it in c# code using a bool that gets ignored

  • John McPhersonMicrosoft employee

    I suppose the value of this over the previously detailed event that returns no result is for use in generic code.

    Would be great if this article was not needed due to Regular void. To me, an enthusiast but far from language expert, it makes so much sense to have void be a complete type and reduce the need for these special cases.

    Read more