929. Thread constructor

Section: 33.3.2.2 [thread.thread.constr] Status: C++11 Submitter: Anthony Williams Opened: 2008-10-23 Last modified: 2016-02-10

Priority: Not Prioritized

View all other issues in [thread.thread.constr].

View all issues with C++11 status.

Discussion:

Addresses UK 323

The thread constructor for starting a new thread with a function and arguments is overly constrained by the signature requiring rvalue references for func and args and the CopyConstructible requirements for the elements of args. The use of an rvalue reference for the function restricts the potential use of a plain function name, since the type of the bound parameter will be deduced to be a function reference and decay to pointer-to-function will not happen. This therefore complicates the implementation in order to handle a simple case. Furthermore, the use of rvalue references for args prevents the array to pointer decay. Since arrays are not CopyConstructible or even MoveConstructible, this essentially prevents the passing of arrays as parameters. In particular it prevents the passing of string literals. Consequently a simple case such as

void f(const char*);
std::thread t(f,"hello");

is ill-formed since the type of the string literal is const char[6].

By changing the signature to take all parameters by value we can eliminate the CopyConstructible requirement and permit the use of arrays, as the parameter passing semantics will cause the necessary array-to-pointer decay. They will also cause the function name to decay to a pointer to function and allow the implementation to handle functions and function objects identically.

The new signature of the thread constructor for a function and arguments is thus:

template<typename F,typename... Args>
thread(F,Args... args);

Since the parameter pack Args can be empty, the single-parameter constructor that takes just a function by value is now redundant.

[ Howard adds: ]

I agree with everything Anthony says in this issue. However I believe we can optimize in such a way as to get the pass-by-value behavior with the pass-by-rvalue-ref performance. The performance difference is that the latter removes a move when passing in an lvalue.

This circumstance is very analogous to make_pair (23.4 [pairs]) where we started with passing by const reference, changed to pass by value to get pointer decay, and then changed to pass by rvalue reference, but modified with decay<T> to retain the pass-by-value behavior. If we were to apply the same solution here it would look like:

template <class F> explicit thread(F f);
template <class F, class ...Args> thread(F&& f, Args&&... args);

-4- Requires: F and each Ti in Args shall be CopyConstructible if an lvalue and otherwise MoveConstructible. INVOKE(f, w1, w2, ..., wN) (23.14.3 [func.require]) shall be a valid expression for some values w1, w2, ... , wN, where N == sizeof...(Args).

-5- Effects: Constructs an object of type thread and executes INVOKE(f, t1, t2, ..., tN) in a new thread of execution, where t1, t2, ..., tN are the values in args.... Constructs the following objects in memory which is accessible to a new thread of execution as if:

typename decay<F>::type g(std::forward<F>(f));
tuple<typename decay<Args>::type...> w(std::forward<Args>(args)...);

The new thread of execution executes INVOKE(g, wi...) where the wi... refers to the elements stored in the tuple w. Any return value from g is ignored. If f terminates with an uncaught exception, std::terminate() shall be called. If the evaluation of INVOKE(g, wi...) terminates with an uncaught exception, std::terminate() shall be called [Note: std::terminate() could be called before entering g. -- end note]. Any exception thrown before the evaluation of INVOKE has started shall be catchable in the calling thread.

Text referring to when terminate() is called was contributed by Ganesh.

[ Batavia (2009-05): ]

We agree with the proposed resolution, but would like the final sentence to be reworded since "catchable" is not a term of art (and is used nowhere else).

[ 2009-07 Frankfurt: ]

This is linked to N2901.

Howard to open a separate issue to remove (1176).

In Frankfurt there is no consensus for removing the variadic constructor.

[ 2009-10 Santa Cruz: ]

We want to move forward with this issue. If we later take it out via 1176 then that's ok too. Needs small group to improve wording.

[ 2009-10 Santa Cruz: ]

Stefanus provided revised wording. Moved to Review Here is the original wording:

Modify the class definition of std::thread in 33.3.2 [thread.thread.class] to remove the following signature:

template<class F> explicit thread(F f);
template<class F, class ... Args> explicit thread(F&& f, Args&& ... args);

Modify 33.3.2.2 [thread.thread.constr] to replace the constructors prior to paragraph 4 with the single constructor as above. Replace paragraph 4 - 6 with the following:

-4- Requires: F and each Ti in Args shall be CopyConstructible if an lvalue and otherwise MoveConstructible. INVOKE(f, w1, w2, ..., wN) (23.14.3 [func.require]) shall be a valid expression for some values w1, w2, ... , wN, where N == sizeof...(Args).

-5- Effects: Constructs an object of type thread and executes INVOKE(f, t1, t2, ..., tN) in a new thread of execution, where t1, t2, ..., tN are the values in args.... Constructs the following objects:

typename decay<F>::type g(std::forward<F>(f));
tuple<typename decay<Args>::type...> w(std::forward<Args>(args)...);

and executes INVOKE(g, wi...) in a new thread of execution. These objects shall be destroyed when the new thread of execution completes. Any return value from g is ignored. If f terminates with an uncaught exception, std::terminate() shall be called. If the evaluation of INVOKE(g, wi...) terminates with an uncaught exception, std::terminate() shall be called [Note: std::terminate() could be called before entering g. -- end note]. Any exception thrown before the evaluation of INVOKE has started shall be catchable in the calling thread.

-6- Synchronization: The invocation of the constructor happens before the invocation of f g.

[ 2010-01-19 Moved to Tentatively Ready after 5 positive votes on c++std-lib. ]

Proposed resolution:

Modify the class definition of std::thread in 33.3.2 [thread.thread.class] to remove the following signature:

template<class F> explicit thread(F f);
template<class F, class ... Args> explicit thread(F&& f, Args&& ... args);

Modify 33.3.2.2 [thread.thread.constr] to replace the constructors prior to paragraph 4 with the single constructor as above. Replace paragraph 4 - 6 with the following:

Given a function as follows:


template<typename T> typename decay<T>::type decay_copy(T&& v)
    { return std::forward<T>(v); }

-4- Requires: F and each Ti in Args shall be CopyConstructible if an lvalue and otherwise satisfy the MoveConstructible requirements. INVOKE(f, w1, w2, ..., wN) (23.14.3 [func.require]) shall be a valid expression for some values w1, w2, ... , wN, where N == sizeof...(Args). INVOKE(decay_copy(std::forward<F>(f)), decay_copy(std::forward<Args>(args))...) (23.14.3 [func.require]) shall be a valid expression.

-5- Effects: Constructs an object of type thread and executes INVOKE(f, t1, t2, ..., tN) in a new thread of execution, where t1, t2, ..., tN are the values in args.... Any return value from f is ignored. If f terminates with an uncaught exception, std::terminate() shall be called. The new thread of execution executes INVOKE(decay_copy(std::forward<F>(f)), decay_copy(std::forward<Args>(args))...) with the calls to decay_copy() being evaluated in the constructing thread. Any return value from this invocation is ignored. [Note: this implies any exceptions not thrown from the invocation of the copy of f will be thrown in the constructing thread, not the new thread. — end note]. If the invocation of INVOKE(decay_copy(std::forward<F>(f)), decay_copy(std::forward<Args>(args))...) terminates with an uncaught exception, std::terminate shall be called.

-6- Synchronization: The invocation of the constructor happens before the invocation of the copy of f.