Home > Enterprise >  Why is converting constructor of std::packaged_task explicit?
Why is converting constructor of std::packaged_task explicit?

Time:11-13

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

CodePudding user response:

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.

  • Related