2

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?

glades
  • 3,778
  • 1
  • 12
  • 34
  • I see no [assignment](https://en.cppreference.com/w/cpp/thread/packaged_task/operator%3D) overload for a callable on the right hand site. And callables are not implicitly convertible to packaged tasks since all [constructors](https://en.cppreference.com/w/cpp/thread/packaged_task/packaged_task) accepting a callable are explicit. – Pepijn Kramer Jan 19 '23 at 12:56
  • The packaged_task will however accept any type via universal reference constructor: template explicit packaged_task( F&& f ); Why does it not accept a lambda then, even if it is explicit? – glades Jan 19 '23 at 13:07
  • AFIAK template functions are not considered when type conversions come into play. Your right hand side will first need to be converted to a packaged task, before the template is considered – Pepijn Kramer Jan 19 '23 at 13:13
  • @PepijnKramer not sure but I think you refer to conversions not being considered for template argument deduction. Though, thats not relevant here, no conversion is necessary for deduction as `T` matches any type, it also matches the type of the lambda – 463035818_is_not_an_ai Jan 19 '23 at 13:25

1 Answers1

5

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 ).

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • I guess I don't understand what the difference between bar b(f) and bar b = f is in this case. How is one different from the other? – glades Jan 19 '23 at 13:04
  • 2
    @glades, that's what `explicit` does: https://en.cppreference.com/w/cpp/language/explicit – ChrisMM Jan 19 '23 at 13:06
  • 2
    @glades `bar b= f;` requires a conversion to be avialble, there is but you would need to call it explicitly, as in `bar b = bar(f);`. With `bar b(f)` you are calling the constructor explicitly – 463035818_is_not_an_ai Jan 19 '23 at 13:07
  • @ChrisMM Ah I see... But on the other hand why do I even need to convert something, there's a constructor `template explicit packaged_task( F&& f );` that takes anything? – glades Jan 19 '23 at 13:12
  • @glades its in the section you quote "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 ...". The lambda is not a `std::packaged_task` – 463035818_is_not_an_ai Jan 19 '23 at 13:13
  • @463035818_is_not_a_number I don't understand. Couldn't I deduce `template explicit packaged_task( F&& f );` as F = main::lambda_xy and have a perfectly fitting constructor? – glades Jan 19 '23 at 13:17
  • 1
    @glades the constructor does fit perfectly, but it is explicit. It is not considered for implicit conversions. – 463035818_is_not_an_ai Jan 19 '23 at 13:18
  • @glades maybe I should have used this as examples: https://godbolt.org/z/cbrqc4GrE, without explicit: https://godbolt.org/z/ojnjYKGzv calling it explicitly: https://godbolt.org/z/5vfv4Td6s – 463035818_is_not_an_ai Jan 19 '23 at 13:20
  • @463035818_is_not_a_number So you're saying that even though there exists a constructor that matches our type (lambda) perfectly, it is not considered due to it being a _converting_ constructor AND specified explicit? – glades Jan 19 '23 at 13:48
  • 1
    @glades yes, as Chris mentioned, thats basically what `explicit` means https://en.cppreference.com/w/cpp/language/explicit – 463035818_is_not_an_ai Jan 19 '23 at 13:58
  • 1
    " 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." – 463035818_is_not_an_ai Jan 19 '23 at 13:59
  • @463035818_is_not_a_number Thanks. They should add a note that this is also true for templated constructors. – glades Jan 19 '23 at 14:08
  • 2
    @glades "a constructor" is general enough, it covers non-template and templated constructors – 463035818_is_not_an_ai Jan 19 '23 at 14:09