Evgenii
Evgenii

Reputation: 145

std::packaged_task should have deleted copy c'tor with const parameter

Link https://cplusplus.github.io/LWG/issue2067 provides the following discussion:

Class template packaged_task is a move-only type with the following form of the deleted copy operations: packaged_task(packaged_task&) = delete;
packaged_task& operator=(packaged_task&) = delete;
Note that the argument types are non-const. This does not look like a typo to me, this form seems to exist from the very first proposing paper on N2276. Using either of form of the copy-constructor did not make much difference before the introduction of defaulted special member functions, but it makes now an observable difference. This was brought to my attention by a question on a German C++ newsgroup where the question was raised why the following code does not compile on a recent gcc:

#include <utility>
#include <future>
#include <iostream>
#include <thread>

int main() {
  std::packaged_task<void()> someTask([]{ std::cout << std::this_thread::get_id() << std::endl; });
  std::thread someThread(std::move(someTask)); // Error here
// Remainder omitted
}

It turned out that the error was produced by the instantiation of some return type of std::bind which used a defaulted copy-constructor, which leads to a const declaration conflict with [class.copy] p8.

Some aspects of this problem are possibly core-language related, but I consider it more than a service to programmers, if the library would declare the usual form of the copy operations (i.e. those with const first parameter type) as deleted for packaged_task to prevent such problems.

Could anybody explain the meaning of the marked statement? I don't undestand how the missing const qualifer affects the compilation process, and how this behavior is explained in standard. What is the point of adding const to the parameter of the deleted copy constructor?

Upvotes: 3

Views: 236

Answers (1)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275500

Here is a toy example:

struct problem {
  problem()=default;
  problem(problem&&)=default;
  problem(problem&)=delete;
};

template<class T>
struct bob {
  T t;
  bob()=default;
  bob(bob&&)=default;
  bob(bob const&)=default;
};

int main() {
  problem p;
  problem p2 = std::move(p);
  bob<problem> b;
  bob<problem> b2 = std::move(b);
}

bob<problem> fails to compile because the bob(bob const&)=default errors out when it interacts with problem(problem&)=delete.

Arguably the standard "should" error-out cleanly when it determines that it cannot implement bob(bob const&), and treat the =default as =delete (like it would if we had problem(problem const&)=delete), but the standard wording isn't going to be flawless in this corner case of a corner case. And this corner of a corner case is going to be strange and quirky enough that I'm not certain a general rule that makes it translate =default to =delete would be right!

The fix if we problem(problem const&)=delete (well, to packaged_task) is going to be so much cleaner than anything we do to =default ctor rules.

Now standard delving:

First, it is obvious that the implicitly declared copy constructor of bob<problem> above is going to have signature bob(bob&) in [class.ctor]. I won't even quote the standard for that, because lazy.

We go and explicitly default bob(bob const&) copy ctor, which differs in signature from the one that would be implicitly declared.

There are rules about explicitly defaulting functions and their conflict with the signatures is in 11.4.2.

In Explicitly-defaulted functions[dcl.fct.def.default] 11.4.2/2

2 The typeT1of an explicitly defaulted function F is allowed to differ from the type T2 it would have had if it were implicitly declared, as follows:

—(2.1) T1 and T2 may have differing ref-qualifiers; and

—(2.2) if T2 has a parameter of type const C&, the corresponding parameter of T1 may be of type C&.

If T1 differs from T2 in any other way, then:

—(2.3) if F is an assignment operator, and the return type of T1 differs from the return type of T2 or T1’s parameter type is not a reference, the program is ill-formed;

—(2.4) otherwise, if F is explicitly defaulted on its first declaration, it is defined as deleted;

—(2.5) otherwise, the program is ill-formed.

The defaulted one is T1, which contains const& not &, so (2.2) doesn't apply.

My reading actually has it getting caught on (2.4); the type of bob(bob const&) differs from the implicitly declared bob(bob&) in an impermissible way; but first declaration is defaulted, so it should be deleted.

I'm looking at the n4713 draft version; maybe an older version didn't have that clause.

Upvotes: 1

Related Questions