The need for std::unique_function

281 views
Skip to first unread message

Ryan McDougall

unread,
Oct 22, 2018, 2:19:02 PM10/22/18
to std-pr...@isocpp.org

“This is motivated by increasing usage of things like executors and the task

queue where it is useful to embed move-only types like a std::promise within

a type erased function. That isn't possible without this version of a type

erased function.” -- Chandler Carruth[1]

Thanks

“std::function and Beyond“ N4159, “Qualified std::function Signatures” P0045R1, and “A polymorphic wrapper for all Callable objects (rev. 3)” P0288R1 et al. for laying the groundwork for this paper. Thank you to David Krauss and Arthur O’Dwyer for your discussion and feedback. Thank you all authors of alternative type erased callable containers for your proof and inspiration.

Motivation

std::function models both CopyConstructible and CopyAssignable, and requires its erased target type to declare a copy constructor. This means std::function  is hopelessly incompatible with std::unique_ptr, std::promise, or any other move-only type. This is a functional gap felt by C++ users to the degree that there’s some half-dozen popular and high-quality implementations available under the name “unique_function”. [2][3][4][5][6]


C++ has many move-only vocabulary types, and when introduced they impose tighter constraints on the interface, and can become “viral” -- causing any previously copyable paths to require move or forwarding operations. Consider


class DbResult {

private:

 std::unique_ptr<Blob> data_;  // now required!

};


class Reactor {

private:

 std::map<std::string, std::function<void()>> reactions_;

};


reactor.on_event(“db-result-ready”, [&] {

 reactor.on_event(“has-towel”, [result = std::move(db.result())] {

   auto meaning = find(result); // 42

 });

});


It is not enough to simply std::move the DbResult into the lambda, as Reactor::on_event is unable to assign to a move-only lambda as implemented with std::function.


This is a recurring pattern in much concurrent code, such as work queues, task graphs, or command buffers. The committee implicitly understood the need when it created std::packaged_task, also a type-erased polymorphic container, but that type is tightly bound to std::futures, which may not be fit for purpose in any code base that doesn’t already rely on them.


If we are developing any kind of asynchronous work queue we need

  1. inheritance based polymorphism (and a mechanism to manage heap allocated derived objects, such as std::shared_ptr)

  2. type-erased container with small object optimization like std::function (for copy-only callable types), std::packaged_task+std::future (for move-only callable types)


However if any facet of our runtime precludes use of std::future -- such as if it provides its own future type, or does not use futures at all, we are again left without the ability to use std::unique_ptr or any other non-copyable type.


auto unique =

 std::make_unique<BankTransfer>(“DrEvil”, 1000000000);


auto do_bank_transfer =

 [transfer = std::move(unique)] (Bank* to, Bank* from) {

   return from->send(transfer, to);

};


ThreadSafeQueue<std::function<int()>> transactions1;

transactions1.emplace(do_bank_transfer);  // Compile Error!!


// ...


ThreadSafeQueue<std::packaged_task<int()>> transactions2;

ThreadSafeQueue<int> results

transactions2.emplace(do_bank_transfer);


hpy::async([&] {

 while (!transactions2.empty()) {

   transactions2.top()();

   results.push_back(transactions2.top().get_future()); // ??

 }

});


In the above example we simply present wasted human time and computational time due to an unnecessary synchronization with std::future, however a more complex system may indeed need their own future implementation which std::packaged_task cannot interoperate with at all.

Comparison Table

std::promise<Foo> p;

std::unique_ptr<Foo> f;

std::queue<std::function<void()>> a;

std::queue<std::unique_function<void()>> b;

using namespace std;



Before Proposal

After Proposal

1

auto s = make_shared<Foo>(move(f));
a.emplace([s] { s->work(); });


// ...

auto shared = a.top();

shared();

b.emplace([u = move(f)] {

 u->work();

});

// ...

auto unique = move(b.top());

unique();

2

a.emplace([r = f.get()] {

 r->work();

});

b.emplace([u = move(f)] {

 u->work();

});

3

atomic<Foo*> result{nullptr};

a.emplace([&result] {

 result = new Foo;

});
// ...
spin_on(result);
result->work();

auto future = p.get_future();

b.emplace([p = move(p)] {

 p.set_value(Foo{});

});

// ...

future.get().work();


As you can see, attempts to work around the limitation of std::function results in unacceptable undermining of uniqueness semantics, life-time safety, and/or ease of use:

  1. Loss of move-only semantics and extra LoC and a heap allocation per shared pointer.

  2. Queue lifetime must not exceed enclosing scope lifetime (or dangling pointer).

  3. Poor usability or unnecessary complexity in search of better performance.

Alternatives

Papers “std::function and Beyond“ N4159, “A polymorphic wrapper for all Callable objects (rev. 3)” P0288R1 have already argued for fixing std::function to support non-copyable types (among other issues) some time ago. Yet it seems self evident that as long and as widely as std::function has been in use, any change that breaks the observable surface area of std::function is a non-starter. The question is would the use case we outlined herein, if overlaid onto the existing std::function, cause previously valid code to break, or conversely previously incorrect code to become easily written?


Let’s assume we have a means of “fixing” std::function to allow mixing of copy and move semantics. Clearly all existing code would continue to function as all existing target callable types are CopyConstructible and CopyAssignable, but what if we mix erased instances with copy and move semantics? How should the following operations on std::function be defined in terms of their target callable types?


Let’s temporarily ignore the details of memory management, and consider p to be the underlying pointer to the erased instance.


std2::function<void()> c = C{}; // Copy-only
std2::function<void()> m = M{}; // Move-only



Operation

Definition

Result

c = std::move(m);

*((M*)c.p) = move(*((M*)m.p));

Move

m = std::move(c);

*((C*)c.p) = move(*((C*)m.p));

Copy

c = m;

*((M*)c.p) = *((M*)m.p);

Throw ???

m = c;

*((C*)c.p) = *((C*)m.p);

Copy


Again we face an unacceptable undermining of uniqueness semantics; in addition we have changed the observable surface area of assignment by necessitating a new runtime error reporting mechanism for when erased targets conflicting behavior, affecting the exception-safety of existing code.

Shallow v. Deep Const

“Shallow” or “Deep” const in a type erased context means whether the constness of the erasing type is extended towards the erased type. For our discussion we consider whether the the erasing container is const callable if and only if the underlying type is const callable.


It is by now understood that the standard requires const correctness to imply thread-safety, and if the container operator() is const, the underlying callable type’s operator() must also be const in order to hope of satisfying this obligation. So a shallow const container could not admit a thread-safe call operation in general, and both N4159 and P0045R1 draw attention to the unfortunate outcome. The solution presented in those papers it to include the constness of the callable in signature, and to have the constness of container operator() be conditional on the signature.

struct Accessor {

 void operator()() const;

 // ...

} access;


struct Mutator {

 void operator()();

 // ...

} mutate;


std3::function<void() const> a = access;  // ok

std3::function<void()> b = access;        // ok

std3::function<void()> c = mutate;        // ok

std3::function<void() const> d = mutate;  // compile error


a = b;  // compile error: target type more restrictive

b = a;  // ok

b = c;  // ok


This proposal, while in the author’s opinion is highly recommended improvement, it’s best presented in referenced papers, and for simplicity’s sake isn’t pursued further here.


Otherwise, without the above but desiring deep const semantics, we would wish that const containers require const callable targets. However, since we have erase the constness of target instance, we are left with throwing when deep const is violated, breaking existing exception-safe code.


const std4::function<void()> a;    // deep const

std::function<void()> b = mutate;  // non-const target

a = b;                             // target copied

a();                               // now throws!

Necessity and Cost

We can all agree type erased containers should support as much as feasible the const correctness of its target types, but we began our argument with a specific asynchronous use case that involved deferred computations, often invoked on foreign threads. If this pattern as seen “in the wild” makes use of thread safe queues, is thread safety of the container itself actually sufficient to justify the costs associated with extra synchronization or exception safety? Even if we can guarantee const container only calls const target methods, we still cannot guarantee the target itself makes the connection between const and thread safety.


Should a proposed std::unique_function emulate “broken” shallow const correctness of std::function for uniformity, or is “fixing” the contradiction worth breaking runtime changes? And is std::unique_/function more like a container or pointer (where const is understood to be shallow), or is it more like an opaque type (where const is generally considered to be deep)? Historically we allow container const to vary independently of parameterized types when they exist, suggesting std::function should remain a shallow container.


While this question is relevant to the specification of std::unique_function, it is ultimately orthogonal to the question of the need for std::unique_function to exist, which is this paper’s main concern.

Conclusion

So long as we have move-only lambdas and type erased callable containers, a move-only capable std::function is required. As uniqueness semantics of container are orthogonal to const-correctness of the interface we recommend not conflating the two, and pursuing P0045R1 for const-correctness as an independent feature request.

References

[1] https://reviews.llvm.org/D48349

[2] http://llvm.org/doxygen/FunctionExtras_8h_source.html#l00046

[3] https://github.com/STEllAR-GROUP/hpx/blob/master/hpx/util/unique_function.hpp

[4] https://github.com/potswa/cxx_function/blob/master/cxx_function.hpp#L1192

[5] https://github.com/Naios/function2/blob/master/include/function2/function2.hpp#L1406

[6] https://github.com/facebook/folly/blob/master/folly/Function.h


Lee Howes

unread,
Oct 23, 2018, 12:14:49 PM10/23/18
to std-pr...@isocpp.org, bbu...@fb.com
Hi Ryan,
Are you planning to attend the meeting and argue the case for unique_function? This is something that came up again this week for us at Facebook as well, and while we work around it with folly::Function as you noted, having to do so is not optimal. It would certainly be a good thing to get a unique_function into the standard soonish.

Lee Howes

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CAHn%2BA5NyaUQjAeudtObjn08Trz4iJ%2Bu-GGt2J5VnmtmZJiPVxg%40mail.gmail.com.

Ryan McDougall

unread,
Oct 23, 2018, 12:18:01 PM10/23/18
to std-pr...@isocpp.org, bbu...@fb.com
Yes, I'll be at SD in order to push this paper -- thought I don't know if there's room on anyone schedule. I have hope however since it's relatively uncontroversial it can yet be fast tracked. I would very much like help with wording if you're available.

Cheers,

Lee Howes

unread,
Oct 23, 2018, 12:26:20 PM10/23/18
to std-pr...@isocpp.org, bbu...@fb.com
From the minutes from the presentation of P0288 in Toronto it looks like there was a request for updated wording to make it independent of P0045, but otherwise little obvious concern. It seems likely that that's the important change to make.

Given the paper reading load between now and SD we're going to be overwhelmed, I think. I'd be happy to give some feedback on a wording draft in the short term.

Ryan McDougall

unread,
Oct 23, 2018, 12:32:20 PM10/23/18
to std-pr...@isocpp.org, bbu...@fb.com
I reference P0045, but my paper is independent. Essentially "we need this, it's kind of obvious, let's decide if it's like std::function or "fixed" by P0045".

If you or your team will be at SD we can bang out some wording in an hour I feel.

Lee Howes

unread,
Oct 23, 2018, 12:35:36 PM10/23/18
to std-pr...@isocpp.org, bbu...@fb.com
A new revision of David Krauss's paper seems like the cleanest approach to preserve history in discussions, but yes, we can definitely talk in SD. There will be a few of us there. 

Ryan McDougall

unread,
Oct 23, 2018, 12:37:49 PM10/23/18
to std-pr...@isocpp.org, bbu...@fb.com
My understanding is David doesn't develop C++ anymore. Do you suggest we re-write/update his paper?

On Tue, Oct 23, 2018 at 9:35 AM Lee Howes <xri...@gmail.com> wrote:
A new revision of David Krauss's paper seems like the cleanest approach to preserve history in discussions, but yes, we can definitely talk in SD. There will be a few of us there. 

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.

Lee Howes

unread,
Oct 23, 2018, 12:57:22 PM10/23/18
to std-pr...@isocpp.org, bbu...@fb.com
Well that was what I was thinking, but it's really up to you. I'm probably not the right person to advise on LEWG procedure so I could easily be giving the wrong advice anyway.

Brian Budge

unread,
Oct 23, 2018, 1:35:39 PM10/23/18
to std-pr...@isocpp.org, Lee Howes
David suggested to reach out to Jeffrey Yasskin on LEWG to loop him into the discussion.


From: Lee Howes <xri...@gmail.com>
Sent: Tuesday, October 23, 2018 9:57:09 AM
To: std-pr...@isocpp.org
Cc: Brian Budge
Subject: Re: [std-proposals] The need for std::unique_function
 

David Krauss

unread,
Oct 23, 2018, 4:59:46 PM10/23/18
to Ryan McDougall, Lee Howes, bbu...@fb.com, ISO C++ Standard - Future Proposals
Hi Ryan, all,

Thanks for your support! I’m not an active user of C++ and I won’t be attending the next conference, but anyone can certainly still ping me.

Feel free to carry over the P0288 number. The main work to align the P0288 draft with your proposal would be rebasing the standardese onto the IS, from the current basis of P0045. Most of this can be done by recovering the text from P0288R0.

The rationale for P0288R1 depending on P0045 was that the shallow-const issue was spreading to this new class as a virus, so the fix in P0045 should be applied first to contain the damage. However, two years have passed with less interest in P0045 (or another deep-const fix; adding deep const does not imply all of P0045). The fixed, dependent version P0288R1 was never presented — several volunteers have come forward but the extra complication has been a hindrance.

We might focus now on building LEWG consensus on the order of dependencies. We’ve missed 2018 and there’s still plenty of time before Kona 2019. If some committee members are serious about deep const as a hard requirement, then they have ample opportunity to support P0045 or to invent another solution. Otherwise, everyone should be able to agree on a standalone unique_function.

- Best,
David


--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.

Bryce Adelstein Lelbach aka wash

unread,
Oct 24, 2018, 3:00:01 AM10/24/18
to std-pr...@isocpp.org, Ryan McDougall, Lee Howes, bbu...@fb.com
I would be willing to allocate time in LEWGI at San Diego to see this paper, as it's a frequently requested feature.

This comes with the caveat I'd like one updated proposal for this, intended for discussion ready for circulation on the LEWG reflector by this Friday.


Ryan McDougall

unread,
Oct 24, 2018, 12:41:26 PM10/24/18
to Bryce Adelstein Lelbach, std-pr...@isocpp.org, Lee Howes, bbu...@fb.com
From my point of view there are 2 issues:
1) Do we need std::unique_function: Yes
2) Do we want it to be move-only version of std::function, or should it be improved version where
```std::unique_function<void() const> f;``` only admits targets with const callable operators?

I think there's legitimate debate about latter, but it should not preclude the former. Should I just present it as an open question?

Lastly standardese. I don't speak it at all (yet). I can simply lift David's -- is that acceptable?

To recapitulate:
- Rename to P0288
- Fold in P0045 as an option
- Steal P0045 standardese
- Ready by friday

^ This is the request?

Bryce Adelstein Lelbach aka wash

unread,
Oct 25, 2018, 5:07:47 AM10/25/18
to Ryan McDougall, std-pr...@isocpp.org, Lee Howes, bbu...@fb.com


On Wed, Oct 24, 2018, 9:41 AM Ryan McDougall <mcdouga...@gmail.com> wrote:
From my point of view there are 2 issues:
1) Do we need std::unique_function: Yes
2) Do we want it to be move-only version of std::function, or should it be improved version where
```std::unique_function<void() const> f;``` only admits targets with const callable operators?




I think there's legitimate debate about latter, but it should not preclude the former. Should I just present it as an open question?

Yes. I have not strong feelings here yet.


Lastly standardese. I don't speak it at all (yet). I can simply lift David's -- is that acceptable?

To recapitulate:
- Rename to P0288
- Fold in P0045 as an option
- Steal P0045 standardese
- Ready by friday

^ This is the request?

Yes.

Ryan McDougall

unread,
Oct 25, 2018, 12:27:24 PM10/25/18
to std-pr...@isocpp.org, Lee Howes, bbu...@fb.com, Bryce Adelstein Lelbach
Ok I still don't have access to the mailing lists and am not sure how long that takes. I assume you can post in my stead until then?

Ryan McDougall

unread,
Oct 25, 2018, 1:46:11 PM10/25/18
to std-pr...@isocpp.org, Lee Howes, bbu...@fb.com, Bryce Adelstein Lelbach

Bryce Adelstein Lelbach aka wash

unread,
Oct 26, 2018, 4:27:03 AM10/26/18
to Ryan McDougall, std-pr...@isocpp.org, Lee Howes, bbu...@fb.com
Indeed.
Reply all
Reply to author
Forward
0 new messages