c++constructorstd-functionexplicitpackaged-task

Why is converting constructor of std::packaged_task explicit?


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();
}

Solution

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