c++packaged-taskcopy-initializationdirect-initialization

Direct initialization != copy initialization when converting from different type?


I always thought direct initialization and copy initialization for types T that do not match the class type are absolutely equal. Yet I seem to be mistaken. The following code doesn't compile if I copy initialize (using = ) and only compiles when I do direct initialization via paranthesis () (in any case the code doesn't work as it terminates, but that's a different story and not relevant for this question).

Demo

#include <future>
#include <cstdio>

int main()
{
    /* This doesn't compile */

    // std::packaged_task<int()> foo = []() -> int {
    //     return 10;
    // };

    /* This works */

    std::packaged_task<int()> foo([]() -> int {
        return 10;
    });

    auto fut = foo.get_future();
    foo();
    auto a = fut.get();
    printf("a == %d\n", a);
}

Error:

<source>: In function 'int main()':
<source>:8:37: error: conversion from 'main()::<lambda()>' to non-scalar type 'std::packaged_task<int()>' requested
    8 |     std::packaged_task<int()> foo = []() -> int {
      |                                     ^~~~~~~~~~~~~
    9 |         return 10;
      |         ~~~~~~~~~~                   
   10 |     };
      |     ~   

cppreference states the following for copy-initialization:

For case T = U:

Otherwise, if T is a class type, and the cv-unqualified version of the type of other is not T or derived from T, or if T is non-class type, but the type of other is a class type, user-defined conversion sequences that can convert from the type of other to T (or to a type derived from T if T is a class type and a conversion function is available) are examined and the best one is selected through overload resolution. The result of the conversion, which is a rvalue temporary (until C++11)prvalue temporary (since C++11)(until C++17)prvalue expression (since C++17) of the cv-unqualified version of T if a converting constructor was used, is then used to direct-initialize the object. The last step is usually optimized out and the result of the conversion is constructed directly in the memory allocated for the target object, but the appropriate constructor (move or copy) is required to be accessible even though it's not used. (until C++17)

As stated here I would expect that the constructor of std::package_task, which takes basically the same invocables as std::function, would make a conversion sequence available in that a lambda can be converted to std::packaged_task, such as is the case for direct initialization. But this doesn't seem to happen. What am I overlooking?


Solution

  • This is due the constructor of std::packaged_task<int()> being explicit. From cppreference/explicit:

    Specifies that a constructor or conversion function (since C++11)or deduction guide (since C++17) is explicit, that is, it cannot be used for implicit conversions and copy-initialization.

    The constructor is a perfect match (template argument T matches anything) but its not a viable user defined conversion sequence (and there are also no other viable conversions from the lambda to std::packaged_task<int()>). It fails for the same reason this does:

    struct foo { };
    
    struct bar {
        explicit bar(foo){}
    };
    
    int main() {
        foo f;
        bar b = f;
    }
    

    Live:

    <source>:9:13: error: conversion from 'foo' to non-scalar type 'bar' requested
        9 |     bar b = f;
          |             ^
    

    While, removing the explicit (https://godbolt.org/z/cPx97zx1e) or using bar b(f); (https://godbolt.org/z/WMYrb18P8) is not an error.

    Note that things do not change when replacing constructor above with a templated one (the error / witout explicit / calling the constructor explicitly ).