0

Why is the converting constructor of std::packaged_task explicit, while the same constructor of std::function is not? I cannot find any reasoning for it.

This, for example, forces casting when passing a lambda as an argument for a function that has a packaged_task (or a reference to it) as a parameter:

void f1(std::function<void()>);
std::future<void> f2(std::packaged_task<void()>);

int main()
{
  f1( []{ } );                                         // ok
  auto fut = f2( []{ } );                              // error
  auto fut = f2( (std::packaged_task<void()>) []{ } ); // ok
  fut.wait();
}
Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • You realise that one ought to get the `future` from the packaged task, right? So the calls to `f2` were most probably bugs to begin with even if compilation succeeded. – ALX23z Nov 12 '21 at 08:53
  • @ALX23z Yes, I do. I changed the code a bit, such that `f2` returns a future for the packaged task. A use case may be a thread pool with enqueuing function that returns `std::future`. – Daniel Langr Nov 12 '21 at 09:07
  • while there's certain truth to the answer - there are also conceptual issues with accepting a `packaged_task` and requiring that nobody has taken a future from yet and returning a future from the call. The interface doesn't make sense - public functions that accept a `packaged_task` as input ought to never take a future from it. Instead, if you plan to return a future, then accept a general callable or a `function/move_only_function` and wrap it with `packaged_task` and return the `future`. – ALX23z Nov 12 '21 at 11:16

1 Answers1

1

Consider following example. Lets create a template class that emulates class with non-explicit templated converting constructor.

#include <iostream>

// Hypothetical overloaded constructor
template <class T>
struct Foo {
    template <class F>
    Foo(F&& f ) { std::cout << "Initialization of Foo \n"; }
    Foo(const Foo& ) { std::cout << "Copy of Foo\n"; }
    Foo(Foo&& ) { std::cout << "Move of Foo\n"; }
};

void bar( Foo<int> f ) {}

int main()
{
    int a = 0;
    std::cout << "1st case\n";
    bar(a);
    std::cout << "2nd case\n";
    bar(Foo<int>(a));   // incorrect behaviour
}

The output will be

1st case
Initialization of Foo 
2nd case
Initialization of Foo 

The template hijacks control in both cases! You actually cannot use copy\move constructors in such situation. A simplest way to avoid it is to make conversion explicit.

#include <iostream>

// Analog of standard's constructor idiom
template <class T>
struct Foo2 {
    
    template <class F>
    explicit Foo2(F&& f ) { std::cout << "Initialization of Foo2 \n"; }
    Foo2(const Foo2& ) { std::cout << "Copy of Foo2\n"; }
    Foo2(Foo2&& ) { std::cout << "Move of Foo2\n"; }
};


void bar2( Foo2<int> f ) {}

int main()
{
    int a = 0;
    
    Foo2<int> f{a};
    std::cout << "\nProper case 1\n";
    // bar2(a); - can't do that
    bar2(Foo2<int>(a));
    std::cout << "Proper case 2\n";
    bar2(f);
    return 0;
}

Output:

Initialization of Foo2 

Proper case 1
Initialization of Foo2 
Proper case 2
Copy of Foo2

std::function is copyable while std::packaged_task have to define custom move constructor and delete copying constructor.

On an attempt to pass std::function , in both cases nothing BAD would happen, the copy and move constructors are likely operate upon function pointer or reference to callable object, std::function is designed to act upon any compatible callable, that includes itself.

if you attempt to do that to do that to std::packaged_task, the converting constructor may do something wrong or likely wouldn't compile because it wouldn't be able to work with instance of own class. A statement that looks like copy (but actually is .. a move? assignment?) would be possible.

Swift - Friday Pie
  • 12,777
  • 2
  • 19
  • 42
  • I understand this problem. But what is the difference from `std::function`, which has both copy and move constructor? – Daniel Langr Nov 12 '21 at 09:41
  • std::function is copyable. – Swift - Friday Pie Nov 12 '21 at 09:42
  • It is. So, basically, you `Foo` is kind of the same as `std::function`. And the described problem should apply to is as well. – Daniel Langr Nov 12 '21 at 09:44
  • @DanielLangr only `std::function` wouldn't care as semantically both cases would be correct for it. I do not exclude idea that `packaged_task` could be redesigned today to actually work correctly in both cases, but its interface was designed for C++11 implementation and barely changed since then – Swift - Friday Pie Nov 12 '21 at 09:55
  • @DanielLangr hmm, can `packaged_task` work with a pointer to a class member? Never tried that but `std::function` can and there are some constructions to support that in code of constructors and `operator()` that have to take a reference to class instance. – Swift - Friday Pie Nov 12 '21 at 10:06
  • Nice, I guess now we are getting somewhere. It really seems that, for some reason, packaged tasks are not constructible form themselves by the converting constructor. Namely, from [cppreference](https://en.cppreference.com/w/cpp/thread/packaged_task/packaged_task): _This constructor does not participate in overload resolution if `std::decay::type` is the same type as `std::packaged_task`._ There seems to be no such restriction with `std::function`. – Daniel Langr Nov 12 '21 at 10:07
  • @DanielLangr I'd be confused if they were constructible from themselves. the object represent a singular task with particular shared state and functional object to create it. Does new constructor create new instance? Does it share the state? OR it's a new task? – Swift - Friday Pie Nov 12 '21 at 10:10
  • Yes, now it makes perfect sense. – Daniel Langr Nov 12 '21 at 10:11