|Project:||Programming Language C++|
This paper proposes resolutions for [LWG3509] and [LWG3510].
The wording below clarifies that the partial application performed by range adaptor objects is essentially identical to that performed by
bind_front. (Indeed, it is effectively a limited version of
bind_back.) In particular, this means that the bound arguments are captured by copy or move, and never by reference. Invocation of the pipeline then either copies or moves the bound entities, depending on the value category of the pipeline.
In other words,
auto c = /* some range */; auto f = /* expensive-to-copy function object */; c | transform(f); // copies f and then move it into the view auto t = transform(f); // copies f c | t; // copies f again from t c | std::move(t); // moves f from t
For all but one range adaptor in the standard library, the bound arguments are expected to be either function objects (which are expected to be cheap to copy and are generally copied freely) or integer-like types (which should be cheap to copy).
views::split, where the pattern can be a range, is an interesting case for two reasons. (The pattern can be an element as well, but that is not particularly interesting.)
If the pattern is a C array, the decay-copy makes it into a pointer. As a result, the
split_view construction might be ill-formed if the user actually meant to use the array as a range. For the most common case of string literals, using the array (including the terminating null character) as the pattern is likely unintended.
If the pattern is an lvalue non-view range, the copy can make it a non-viewable range when the closure object is used as an rvalue:
std::string s = "hello", s1 = "l"; views::split(s, s1); // OK s | views::split(s1); // ill-formed under this wording; copy of s1 forwarded as rvalue auto adapt = views::split(s1); s | adapt; // OK
There does not appear to be a way to avoid this without making the following function template (which demonstrates an expected use of adaptor pipelines) always return something that holds a dangling reference:
In both cases, the workaround is to wrap the pattern in
views::all, which also clearly signifies the reference semantics of the capture. Having a compile-time error, while perhaps less than ideal, seems to be preferable to silent dangling.
As range adaptor objects are customization point objects, and the use of
bind_front-like semantics means that they will be copied and invoked as non-const lvalues and possibly-const rvalues, this wording also resolves [LWG3510] by clarifying that customization point objects are invocable regardless of value category or cv-qualification.
This wording is relative to [N4878].
3 All instances of a specific customization point object type shall be equal ([concepts.equality]). The effects of invoking different instances of a specific customization point object type on the same arguments are equivalent.
4 The type
Tof a customization point object, ignoring cv-qualifiers, shall model
invocable<const T&, Args...>,
invocable<T, Args...>, and
invocable<const T, Args...>(18.7.2 [concept.invocable]) when the types in
Args...meet the requirements specified in that customization point object’s definition. When the types of
Args...do not meet the customization point object’s requirements,
Tshall not have a function call operator that participates in overload resolution. The effect of invoking a customization point object on any particular set of arguments is the same regardless of the cv-qualification or value category of the customization point object.
1 A range adaptor closure object is a unary function object that accepts a
viewable_rangeargument and returns a
view. For a range adaptor closure object
Cand an expression
viewable_range, the following expressions are equivalent and yield a
Given an additional range adaptor closure object
D, the expression
C | Dis well-formed and produces another range adaptor closure object
E. such that the following two expressions are equivalent:
Eis a perfect forwarding call wrapper (20.14.4 [func.require]) with the following properties:
- Its target object is an object
- It has one bound argument entity, an object
- Its call pattern is
argis the argument used in a function call expression of
C | Dis well-formed if and only if the initializations of the state entities of
Eare all well-formed.
2 A range adaptor object is a customization point object (18.104.22.168.6 [customization.point.object]) that accepts a
viewable_rangeas its first argument and returns a
3 If a range adaptor object accepts only one argument, then it is a range adaptor closure object.
4 If a range adaptor object accepts more than one argument, then the following expressions are equivalent:
In this case,
adaptor(args...)is a range adaptor closure object.
4 If a range adaptor object
adaptoraccepts more than one argument, then let
rangebe an expression such that
args...be arguments such that
adaptor(range, args...)is a well-formed expression as specified in the rest of this subclause (24.7 [range.adaptors]), and let
BoundArgsbe a pack that denotes
decay_t<decltype((args))>.... The expression
adaptor(args...)produces a range adaptor closure object
fthat is a perfect forwarding call wrapper with the following properties:
- Its target object is a copy of
- Its bound argument entities
bound_argsconsist of objects of types
- Its call pattern is
adapter(r, bound_args...), where
ris the argument used in a function call expression of
adaptor(args...)is well-formed if and only if the initialization of the bound argument entities of the result, as specified above, are all well-formed.
[LWG3509] Tim Song. Range adaptor objects are underspecified.
[LWG3510] Tim Song. Customization point objects should be invocable as non-const too.
[N4878] Thomas Köppe. 2020-12-15. Working Draft, Standard for Programming Language C++.