On Sunday, February 19, 2017 at 8:49:41 PM UTC-8, Sergey Vidyuk wrote:Sometimes it's required to store std::packaged_task instances with the same argumet types but different result type in the same collection (especially when implementing your own executor-like class). I've started to work on a proposal to provide partial specialization for the std::packaged_task class template which erases result type for the task. The text is here: https://github.com/VestniK/portable_concurrency/blob/result-erased-task-proposal/proposal/proposal.tex pdf version is in attachement.
Github repository provides reference implemetntation for the proposed idea. My implementation uses the same design for the packaged_task as gcc and boost implementations (shared state erases wrapped functor type). Those two implementations can be adjusted to have proposed feature quite easily. Clang implementation requires some extra work since they store type erasure for wrapped function and promise. I haven't investigated Visual Studio implementation so don't know if it's hard to add proposed feature to their standard library.
I would like to ask for feedback on the proposed idea and the document text.(I agree that there's something wonky with the formatting.)I don't understand why we couldn't solve this problem more simply just by making std::function<void(A...)> constructible from std::function<R(A...)>&&, std::future<void> constructible from std::future<T>&&, std::packaged_task<void(A...)> constructible from std::packaged_task<R(A...)>&&, and so on (just in case I missed any).There might be a good reason for not doing that — e.g. it might subvert the type system too much, or it might introduce inefficiency behind the scenes — but I would want to see that you'd considered the possibility and rejected it for reasons, and weren't proposing a new tag type out of "laziness".My intuition is thatstd::packaged_task<int()> pti( [](){ return 42; } );std::future<int> fi = pti.get_future();std::packaged_task<void()> ptv = std::move(pti);ought to be legal, IMHO, because its meaning is "obvious"; it's equivalent tostd::packaged_task<void()> ptv( [pti_ = std::move(pti)](){ pti_(); } );Evenstd::packaged_task<int()> pti( [](){ return 42; } );std::packaged_task<void()> ptv = std::move(pti);std::future<void> fv = ptv.get_future();has an "obvious" meaning IMHO, and I'd like to see it supported if it helps use-cases like Sergey's.
C++ already allows implicit conversion of std::function<T(A...)> to std::function<U(A...)> in exactly the cases I'd consider it appropriate. C++ just doesn't allow it for std::future or std::packaged_task. This suggests to me that the reason is inefficiency-behind-the-scenes, but I don't know if that's an insurmountable obstacle or just nobody's thought up and proposed an efficient solution yet.If I'm missing something and your tag type ignore_t isn't basically interchangeable with void, please let me know. :)–Arthur
Sergey Vidyuk <sir.v...@gmail.com>: Feb 20 08:22PM -0800
вторник, 21 февраля 2017 г., 5:34:55 UTC+7 пользователь Bryce Glover
написал:
> Regards,
> Bryce Glover
> Random...@gmail.com <javascript:>
Spellchecker find terrible mistakes in my text :( attaching fixed version.
What's wrong with formatting on the page 4? The only thing I can see is
template arguments for the intext std::packaged_task<R(A...)> looks like
R(A...) use different font size. Is the issue you are talking about?
Sergey
вторник, 21 февраля 2017 г., 7:03:42 UTC+7 пользователь Arthur O'Dwyer написал:
On Sunday, February 19, 2017 at 8:49:41 PM UTC-8, Sergey Vidyuk wrote:
Sometimes it's required to store std::packaged_task instances with the same argumet types but different result type in the same collection (especially when implementing your own executor-like class). I've started to work on a proposal to provide partial specialization for the std::packaged_task class template which erases result type for the task. The text is here: https://github.com/VestniK/portable_concurrency/blob/result-erased-task-proposal/proposal/proposal.tex pdf version is in attachement. [...]I don't understand why we couldn't solve this problem more simply just by making std::function<void(A...)> constructible from std::function<R(A...)>&&, std::future<void> constructible from std::future<T>&&, std::packaged_task<void(A...)> constructible from std::packaged_task<R(A...)>&&, and so on [...]
My intuition is thatstd::packaged_task<int()> pti( [](){ return 42; } );std::future<int> fi = pti.get_future();std::packaged_task<void()> ptv = std::move(pti);ought to be legal, IMHO, because its meaning is "obvious"; it's equivalent tostd::packaged_task<void()> ptv( [pti_ = std::move(pti)](){ pti_(); } );Evenstd::packaged_task<int()> pti( [](){ return 42; } );std::packaged_task<void()> ptv = std::move(pti);std::future<void> fv = ptv.get_future();has an "obvious" meaning IMHO, and I'd like to see it supported if it helps use-cases like Sergey's.
packaged_task is not just type-erasure for function. It's write end of the single-value thread-safe communication channel with capabilities to notify read channel about value delivery combined together with type-erasure for function with additional protection from calling wrapped function twice. Using it like std::function just because you want to store move-only function object involves 1 (or 2 in case of libc++) extra allocation, 1 extra indirection, thread-safe check if shared state is empty before the call, thread-safe operation of storing a value in the shared state.
The main motivation of this proposal is to provide building block to implement executors (especially when this concept will be published with Concurrency TS 2). For example Implementation of TwoWayExecutor (p0443r1 terminology) which uses std::future for communication channel requires to pack together shared state and function to be executed, take a future from it and erase the result type of the packaged task in order to store it in the execution context till the time it will be executed. My proposal is to provide zero cost way to erase result type of the packaged_task. There are no ways to write such type erasure in the user code right now (any user code implementation will not be zero cost).
On Mon, Feb 20, 2017 at 6:22 PM, Sergey Vidyuk <sir.v...@gmail.com> wrote:вторник, 21 февраля 2017 г., 7:03:42 UTC+7 пользователь Arthur O'Dwyer написал:On Sunday, February 19, 2017 at 8:49:41 PM UTC-8, Sergey Vidyuk wrote:Sometimes it's required to store std::packaged_task instances with the same argumet types but different result type in the same collection (especially when implementing your own executor-like class). I've started to work on a proposal to provide partial specialization for the std::packaged_task class template which erases result type for the task. The text is here: https://github.com/VestniK/portable_concurrency/blob/result-erased-task-proposal/proposal/proposal.tex pdf version is in attachement. [...]I don't understand why we couldn't solve this problem more simply just by making std::function<void(A...)> constructible from std::function<R(A...)>&&, std::future<void> constructible from std::future<T>&&, std::packaged_task<void(A...)> constructible from std::packaged_task<R(A...)>&&, and so on [...]My intuition is thatstd::packaged_task<int()> pti( [](){ return 42; } );std::future<int> fi = pti.get_future();std::packaged_task<void()> ptv = std::move(pti);ought to be legal, IMHO, because its meaning is "obvious"; it's equivalent tostd::packaged_task<void()> ptv( [pti_ = std::move(pti)](){ pti_(); } );Evenstd::packaged_task<int()> pti( [](){ return 42; } );std::packaged_task<void()> ptv = std::move(pti);std::future<void> fv = ptv.get_future();has an "obvious" meaning IMHO, and I'd like to see it supported if it helps use-cases like Sergey's.
packaged_task is not just type-erasure for function. It's write end of the single-value thread-safe communication channel with capabilities to notify read channel about value delivery combined together with type-erasure for function with additional protection from calling wrapped function twice. Using it like std::function just because you want to store move-only function object involves 1 (or 2 in case of libc++) extra allocation, 1 extra indirection, thread-safe check if shared state is empty before the call, thread-safe operation of storing a value in the shared state.I know std::packaged_task has more machinery behind it than std::function. If you want to see my level of knowledge on the subject (which is to say "above average but still largely superficial" ;)) I've got a CppCon 2015 talk where I present an extremely naive std::packaged_task. I don't know much about efficiency, but I'm familiar with its functionality.Your proposal involves non-trivial library changes. Maybe I should have been explicit that my "counterproposal" of adding implicit conversions also involves non-trivial library changes. I'm not really arguing about the behind-the-scenes implementation of the entity; I'm just suggesting that the exposed interface for the entity ought to be spelled std::packaged_task<void(A...)> instead of std::packaged_task<ignore_t(A...)>.
In part 2 of your paper, your example code uses mt_queue<std::function<void()>> instead of mt_queue<unique_function<void()>>, which might cause you to incur extra copies. You should know that I consider that issue "solved", because I believe that std::unique_function will be introduced in C++20 and/or every relevant codebase will have their own my::unique_function by then.
The way I would write your part-2 code is
using task_queue = mt_queue<std::packaged_task<void()>>;template<typename F>
auto post_function(task_queue& queue, F&& func){
using R = std::result_of_t<F()>;auto task = std::packaged_task<R()>(func);auto res = task->get_future();queue.push(std::packaged_task<void()>(std::move(task))); // proposes an efficient conversion from packaged_task<R(A...)> to packaged_task<void(A...)>return res;}or evenusing task_queue = mt_queue<std::unique_function<void()>>; // proposes N4543's std::unique_functiontemplate<typename F>
auto post_function(task_queue& queue, F&& func){
using R = std::result_of_t<F()>;auto task = std::packaged_task<R()>(func);auto res = task->get_future();queue.push(std::packaged_task<void()>(std::move(task)).get_function()); // proposes an efficient member function packaged_task::get_functionreturn res;}The relevant thing here, the reason I'm bikeshedding so hard, is that I consider it valuable to continue using the keyword "void" to mean "this function returns nothing of interest". I don't like that your proposal makes up a new way to spell "this function returns nothing of interest" — you're spelling it ignore_t() instead of void().The main motivation of this proposal is to provide building block to implement executors (especially when this concept will be published with Concurrency TS 2). For example Implementation of TwoWayExecutor (p0443r1 terminology) which uses std::future for communication channel requires to pack together shared state and function to be executed, take a future from it and erase the result type of the packaged task in order to store it in the execution context till the time it will be executed. My proposal is to provide zero cost way to erase result type of the packaged_task. There are no ways to write such type erasure in the user code right now (any user code implementation will not be zero cost).I agree that zero-cost-ness is important, and I repeat that I have no idea whether my "counterproposals" can be made zero-cost (but I want to see somebody explain why they aren't and can't be).I agree that this problem cannot be solved by using user code to bolt anything onto std::packaged_task; it requires non-trivial standard library changes. Right now, since std:: provides no one-stop-shop solution, the only efficient solution is for everyone to implement their own everything — my::unique_function and my::future compose to provide my::packaged_task, and so on.My impression is that this is the same situation we were in with std::future prior to the introduction of std::future::then. Back then, anybody who wanted ::then had to implement my::future from scratch in order to get it, because you could not use user code to bolt ::then onto std::future in a zero-cost way.
Hope this clarifies my position,–Arthur
Personally I like the idea to spell result erased task as void(A...) task instead of ignore_t(A...) but isn't it breaking change? C++11 requires that the following code will not throw future_already_retrieved error:
std::packaged_task<std::string(int)> task1([](int x) {return std::to_string(x);});
auto f1 = task1.get_future();
std::packaged_task<void(int)> task2(std::move(task1));
auto f2 = task2.get_future();
unique_function will eliminate one unnecessary allocation: ugly make_shared<packaged_task<R(A...)>> but there is still one allocation
to perform type-erasure unless there is strict requirement on this class to have embedded storage with enough space to store a packaged_task. Such requirement might be reasonable. unique_function proposes "no double wrapping" optimization for std::function. Adding same requirement for packaged_task makes unique_function ultimate function type erasure which provides best efficiency for any standard function (raw function pointer) and function wrappers (both existing function type erasures in the standard library). With such requirements there is no need in my proposal at all.
On Feb 22, 2017, at 3:04 PM, std-pr...@isocpp.org wrote:Sergey Vidyuk <sir.v...@gmail.com>: Feb 22 08:29AM -0800
среда, 22 февраля 2017 г., 7:26:35 UTC+7 пользователь Bryce Glover написал:
(snipped…)
I’ve seen the issue you are talking about in the google drive pdf viewer. There are no such formatting issues when I open the document with ocular on my local computer. I'll play with the styles to fix this issue a little bit later.
Sergey
But I guess my::packaged_task<R()> could be implemented as a unique_ptr<pair<...>> without loss of efficiency. In fact, storing the functor (the "task") in the shared state would increase efficiency by removing the requirement to move-construct the "task" every time the packaged_task itself is move-constructed.
Right now, N4618 30.6.9.1 [futures.task.members]/6 and /10 are very clear that the "stored task" must be move-constructed, swapped, etc., every time those operations are performed on the std::packaged_task; if you want cheaply movable or swappable packaged_tasks you have to implement your own my::packaged_task. (In particular, if you want noexcept-swappable packaged_tasks, I think. N4618's swap is declared noexcept, but you can easily write a swap for your stored task type that throws an exception, which means that swapping packaged_tasks containing those tasks ought to result in a call to std::terminate.)
Effects: exchanges the shared states and stored tasks of *this and other.
Postcondition: *this has the same shared state and stored task (if any) as other prior to the call to swap. other has the same shared state and stored task (if any) as *this prior to the call to swap.