bind_front should not unwrap reference_wrapper (original) (raw)
1. Introduction
This paper proposes a change in the behaviour of the std::bind_front in regards to the bound arguments of the std::reference_wrapper<T> type — the arguments should not be unwrapped (passing rw.get() to underlying callable), and should be propagated unchanged instead (passing rw unmodified to underlying callable).
This change is the result of analysis of use case provided by the Abseil team, that I have not considered during the initial design. The main motivation of the change is to reduce the extent of damage that may be implied by code that assumes "incorrect" behavior.
| Before: bind_front has unwrapping semantic | After: bind_front has propagation semantic |
|---|---|
| Code that assumes propagation semantic may: not compile (static error), create silent compies (performance impact), use dangling references (undefined behavior). | Code that assumes unwrapping semantic may: not compile (static error). |
| Fixing code requires the implementation of a custom binder. | Fixing code requires a cast to desired view type. |
Changes proposed in this paper need to be considered in the C++20 timeline, as they would constitute breaking change after the publication of standard in the current form.
2. Revision history
2.1. Revision 0
Initial revision.
3. Motivation and Scope
This paper discusses the behavior of the code that uses std::bind_front in conjunction with std::reference_wrapper instantations. To illustrate lets consider the following code:
Thingy thingy; auto boundFunctor = std::bind_front(func, std::ref(thingy));
There are two possible and conflicting behaviors of the boundFunctor() invocation (for purpose of presentation we refer to bound reference_wrapper as thingyRef):
- unwrapping:
func(thingyRef.get()), i.e. passingThingy &— mimics behaviour ofstd::bind_frontw.r.t.reference_wrapper, - propagation:
func(thingyRef), i.e. passingstd::reference_wrapper<Thingy>—std::reference_wrapperis treated as any other class. The difference between above behavior is subtle and manifest itself only in specific corner case — it's is usually not observable in the user code, due to the implicit conversion fromstd::reference_wrapper<Thingy>toThingy&.
3.1. Unwrapping use case: Double conversion
One of the situations when the difference between unwrapping and propagation semantic is visible, is when the functor is bound with the reference to the object (e.g. std::reference_wrapper<std::string>, and the code accepts an view to it (e.g. std::string_view). To illustrate lets consider:
void functionAcceptingStringView(std::string_view); void functionAcceptingSpanOfIntegers(std::span);
std::string s; auto fs = std::bind_front(&functionAcceptingStringView, std::ref(s)); std::vector v; auto fv = std::bind_front(&functionAcceptingSpanOfIntegers, std::ref(v));
With the unwrapping (current) semantic, both fs() and fv() compiles correctly, as std::string_view can be implicitly constructed from std::string& andstd::span<int> can be implicitly constructed from std::vector<int>&. In case if propagation semantic was provided, both invocation would not compile, as they would require two user defined conversion to be performed:
std::reference_wrapper<std::string>→std::string&→std::string_viewforfs(), andstd::reference_wrapper<std::vector<int>>→std::vector<int>&→std::span<int>forfv(). However, both of above compilation issues can be easily fixed by binding the desired view type instead of usingstd::ref:
auto fs = std::bind_front(&functionAcceptingStringView, std::string_view(s)); auto fv = std::bind_front(&functionAcceptingSpanOfIntegers, std::span(v));
3.2. Propagation use case: Function currying
The difference between the propagation is unwrapping semantic, is that the former support rebinding of the argument. This can be best illustrated with the implementation of the functor PartialApply that implements function currying in C++:
template struct PartialApply { PartialApply(F f) : f(f) {} F f;
template <typename... A> auto operator()(A const&... a) const {
if constexpr (std::is_invocable<F const&, A const&...>::value) {
return f(a...);
} else {
return bind_front(*this, a...);
}
}};
The intent of the above code is that the expression PartialApply(func)(a)(b) is either:
- calling
func(a, b)if that is well-formed, - returning callback that binds
funcwithaand b and accepts consecutive arguments (rebounding).
The above implementation works flawlessly in case of the propagation semantic, however, it fails in the case of unwrapping is used — in the case when rebinding is performed, copy of arguments that were originally passed via std::ref is made.
In the most optimistic scenario the above issue will manifest as compilation error. This happens when move only type is passed by reference:
std::unique_ptr thingy; auto func = [](std::unique_ptr&, int) {};
PartialApply(func)(std::ref(thingy))(10);
In the case of copyable types, silently copy of the object will be created. This, of course, may have negative performance implication, but in the worst case can lead to dangling references:
std::string str; auto func = [](std::string& s, int) -> std::string& { return s; };
std::string& sref = PartialApply(func)(std::ref(s))(10); // sref refers to copy of str stored in PartialApply sref.push_back('a'); // use of dangling reference
Finally, the author is not aware of the way of fixing PartialApply than reimplementing custombind_front alternative that has propagation semantic.
3.3. Historical design note on LWG 2219
The prominent use case for partial function application functions (like std::bind_front and std::bind), is to provide a helper to compose given method on the class, with a specific object. In case if invocation should be performed on existing object instance (not a copy), a std::reference_wrapper was used as follows:
auto bound = std::bind_front(&Object::method, std::ref(instance));
In the time when the std::bind_front was originally proposed (also applies to std::bind), the above code to work property required unwrapping semantic (at least the for first argument). This was changed with the resolution of LWG 2219: _INVOKE_-ing a pointer to member with a reference_wrapper as the object expression, that have introduced dedicated handling for std::reference_wrapper in _INVOKE_, and unwrapping of std::reference_wrapper is no longer required to achieve this functionality.
4. Design Decisions
4.1. Preference for compilation errors
As indicated in the motivation section, the difference between unwrapping and propagation semantic manifest itself in a very specific scenario. As a consequence, the user, unaware of this specific behavior, may accidentally create an erroneous code that depends on semantic other that one supplied by the standard. In that case, it would be desired that error is detected as early as possible, preferably at compile time.
In light of the above, this paper proposes to switch to propagation semantic, as code that assumes unwrapping becomes ill-formed. With the current behavior (unwrapping), the code that assumes propagation may lead to runtime bugs.
4.2. Preserving other behavior
The change proposed in this paper has no impact on user-visible behaviour of std::bind_front , despite the fact that functors returned by this functor will now store std::reference_wrapper<T> instead of T&, as:
- functors produced by
std::bind_frontare not assignable, so the difference in assignment semantic is not visible to the user, reference_wrapper, like references, is required to be trivially copyable,bind_frontis not currently usable inconstexprcontext.
In addition the P1065: constexpr INVOKE applies constexpr to std::reference_wrapper.
5. Proposed Wording
The proposed wording changes refer to N4810 (C++ Working Draft, 2019-03-15).
Apply following changes to section [func.bind_front] Function template bind_front:
template <class F, class... Args> unspecified bind_front(F&& f, Args&&... args);
In the text that follows:
gis a value of the result of abind_frontinvocation,FDis the typedecay_t<F>,fdis a target object ofg([func.def]) of typeFDinitialized with initializer(std::forward<F>(f)),BoundArgsis a pack that denotes~~std::unwrap_ref_~~decay_t<Args>...,bound_argsis a pack of bound argument entities ofg([func.def]) of typesBoundArgs...initialized with initializers(std::forward<Args>(args))...respectively, andcall_argsis an argument pack used in a function call expression ([expr.call]) ofg,
Update the value of the __cpp_lib_bind_front in table "Standard library feature-test macros" of [support.limits.general] to reflect the date of approval of this proposal.
6. Implementability
This paper can be implemented by simply replacing unwrap_ref_decay_t with decay_t in example implementation from "Implementability" section of the P0365R5: Simplified partial function application paper.
7. Acknowledgements
Titus Winters and Abseil team for providing the feedback on the bind_front and code example that motivated creation of this paper.
Samuel Benzaquen for providing feedback and corrections for this paper.
Special thanks and recognition goes to Sabre (http://www.sabre.com) for supporting the production of this proposal and author's participation in standardization committee.
8. References
- Jonathan Wakely, Issue 2219.
_INVOKE_-ing a pointer to member with areference_wrapperas the object expression, (LWG2219, https://wg21.link/lwg2219) - Barry Revzin,
constexpr _INVOKE_, (P1065, https://wg21.link/p1065) - Richard Smith, "Working Draft, Standard for Programming Language C++" (N4810, https://wg21.link/n4810)
- Tomasz Kamiński, Simplified partial function application, (P0356R5, https://wg21.link/p0356r5)