microsoft / cpprestsdk

The C++ REST SDK is a Microsoft project for cloud-based client-server communication in native code using a modern asynchronous C++ API design. This project aims to help C++ developers connect to and interact with services.
Other
7.91k stars 1.64k forks source link

pplx::when_all crashes with default constructed pplx::tasks #1701

Open garethsb opened 2 years ago

garethsb commented 2 years ago

A default constructed task behaves quite similarly to a task containing an exception, e.g. by pplx::task_from_exception or that has been canceled, in that the default constructed task throws pplx::invalid_operation from wait and get, where a canceled task throws pplx::task_canceled. There are differences, e.g. is_done throws rather than returning true as a canceled task would, but much task-handling code will only use either wait or `get.

However, pplx::when_all does not handle default tasks.

_when_alltest.cpp

#include <iostream>
#include "pplx/pplxtasks.h"

int main()
{
    pplx::task<void> default_task;
    std::cout << "wait:" << std::endl;
    try
    {
        default_task.wait();
    }
    catch (const pplx::invalid_operation& e)
    {
        std::cout << e.what() << '\n';
    }
    std::cout << "when_all:" << std::endl;
    auto tasks = { default_task };
    pplx::when_all(tasks.begin(), tasks.end()).then([](pplx::task<void> finally)
    {
        try
        {
            finally.wait();
        }
        catch (const pplx::invalid_operation& e)
        {
            std::cout << e.what() << '\n';
        }
    }).wait();
    std::cout << "done" << std::endl;
}

Output in gdb:

wait:
wait() cannot be called on a default constructed task.
when_all:

Program received signal SIGSEGV, Segmentation fault.
pplx::details::_WhenAllImpl<void, pplx::task<void> const*>::_Perform (
    _TaskOptions=..., _Begin=0x7fffffffce50, _End=0x7fffffffce60)
    at .../include/pplx/pplxtasks.h:6746
6746                        _JoinAllTokens_add(_MergedSource, _PTasks->_GetImpl()->_M_pTokenState);

I also tried when_any:

<    pplx::when_all(tasks.begin(), tasks.end()).then([](pplx::task<void> finally)
>    pplx::when_any(tasks.begin(), tasks.end()).then([](pplx::task<size_t> finally)

In this case, the call to when_any produces the exception:

  is_apartment_aware() cannot be called on a default constructed task.

At least this doesn't result in a core dump, but wouldn't it be more consistent if that exception were contained in the resulting task rather than thrown from when_any?

garethsb commented 2 years ago

As an additional note, the documentation for pplx::task says that the exception thrown by a default constructed instance is std::invalid_argument. However, pplx::invalid_operation does not have that as a base.