August 12th, 2020

Hidden constraints on the result type in Concurrency Runtime tasks

If you are using the Concurrency Runtime task<T> to represent asynchronous activity, there are some hidden constraints on the type T. If you violate these constraints, the compiler will complain, but perhaps not in an obvious way.ยน

If you try to create a task<T> where T does not have a public default constructor, then you get an error like

ppltasks.h: error C2280: 'Concurrency::details::_ResultHolder<_ReturnType>::_ResultHolder(void)': attempting to reference a deleted function
with _ReturnType = T

And if the T is not copyable, then you get something like

ppltasks.h: Error C2280: 'T::T(const T&) noexcept' attempting to reference a deleted function

What’s going on?

Let’s look at the copyability first. Task results must be copyable because the task result can be consumed multiple times in multiple ways. You can get() multiple times, and each time returns the task result. You can call then() multiple times, and each continuation is given the task result. If the task result were not copyable, then only one of the calls to get() or then() will get the result, and the others would get, um, a letter of apology?

The requirement that the type be publicly constructible is a consequence of the fact that the task contains a copy of the T, and if the task hasn’t completed yet, then the T object needs to contains something, so the library just puts a default-constructed T in it. This requirement is confessed in the source code:

    // this means that the result type must have a public default ctor.
    _ResultHolder<_ReturnType> _M_Result;

Okay, so how can you work around these limitations? We’ll look at that next time.

ยน Related reading: Why does my C++/WinRT project get errors of the form “abi<โ€ฆ>::โ€ฆ is abstract see reference to producer<โ€ฆ>“?, on the underappreciated need for libraries to generate comprehensible error messages when used incorrectly, and the difficulty of compiler error message meta-programming. For these two cases, the Concurrency Runtime could have added some static_asserts to generate custom error messages.

template<typename _Type>
struct _ResultHolder
{
    static_assert(std::is_default_constructible<_Type>::value,
        "The result type of a task must be default constructible");
    static_assert(std::is_copy_constructible<T>::value,
        "The result type of a task must be copy constructible");

    ....
};
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.

3 comments

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

  • Alexis Ryan

    Almost seems like c++ could use constraints on template type parameters similar to generics in C# but with ability specify things like static members and constructors with specific parameters. Interfaces as type constraints good in C# due to the way generics in .net work but templates in c++ would need something more flexible if it were to have constraints.

  • ็ด…ๆจ“้ฎ

    Microsoft may not have a time machine to fix task<T> to use std::variant, but shouldn’t continuations be passed const T & instead of T?