operator()
Document #: | P1169R1 |
Date: | 2021-04-05 |
Project: | Programming Language C++ |
Audience: |
EWG |
Reply-to: |
Barry Revzin <[email protected]> Casey Carter <[email protected]> |
[P1169R0] was presented to EWGI in San Diego, where there was no consensus to pursue the paper. However, recent discussion has caused renewed interest in this paper so it has been resurfaced. R0 of this paper additionally proposed implicitly changing capture-less lambdas to have static function call operators, which would be an breaking change. That part of this paper has been changed to instead allow for an explicit opt-in to static. Additionally, this language change has been implemented.
The standard library has always accepted arbitrary function objects - whether to be unary or binary predicates, or perform arbitrary operations. Function objects with call operator templates in particular have a significant advantage today over using overload sets since you can just pass them into algorithms. This makes, for instance, std::less<>{}
very useful.
As part of the Ranges work, more and more function objects are being added to the standard library - the set of Customization Point Objects (CPOs). These objects are Callable, but they don’t, as a rule, have any members. They simply exist to do what Eric Niebler termed the “Std Swap Two-Step”. Nevertheless, the call operators of all of these types are non-static member functions. Because all call operators have to be non-static member functions.
What this means is that if the call operator happens to not be inlined, an extra register must be used to pass in the this
pointer to the object - even if there is no need for it whatsoever. Here is a simple example:
struct X {
bool operator()(int) const;
static bool f(int);
};
inline constexpr X x;
int count_x(std::vector<int> const& xs) {
return std::count_if(xs.begin(), xs.end(),
#ifdef STATIC
X::f
#else
x
#endif
);
}
x
is a global function object that has no members that is intended to be passed into various algorithms. But in order to work in algorithms, it needs to have a call operator - which must be non-static. You can see the difference in the generated asm btween using the function object as intended and passing in an equivalent static member function:
Even in this simple example, you can see the extra zeroing out of [rsp+15]
, the extra lea
to move that zero-ed out area as the object parameter - which we know doesn’t need to be used. This is wasteful, and seems to violate the fundamental philosophy that we don’t pay for what we don’t need.
The typical way to express the idea that we don’t need an object parameter is to declare functions static
. We just don’t have that ability in this case.
The proposal is to just allow the ability to make the call operator a static member function, instead of requiring it to be a non-static member function. We have many years of experience with member-less function objects being useful. Let’s remove the unnecessary object parameter overhead. There does not seem to be any value provided by this restriction.
There are other operators that are currently required to be implemented as non-static member functions - all the unary operators, assignment, subscripting, conversion functions, and class member access. We do not believe that being able to declare any of these as static will have as much value, so we are not pursuing those at this time. We’re not aware of any use-case for making any of these other operators static, while the use-case of having stateless function objects is extremely common.
There is one case that needs to be specially considered when it comes to overload resolution, which did not need to be considered until now:
struct less {
static constexpr auto operator()(int i, int j) -> bool {
return i < j;
}
using P = bool(*)(int, int);
operator P() const { return operator(); }
};
static_assert(less{}(1, 2));
If we simply allow operator()
to be declared static
, we’d have two candidates here: the function call operator and the surrogate call function. Overload resolution between those candidates would work as considering between:
And currently this is ambiguous because 12.2.4.1 [over.match.best.general]/1.1 stipulates that the conversion sequence for the contrived implicit object parameter of a static member function is neither better nor worse than any other conversion sequence. This needs to be reined in slightly such that the conversion sequence for the contrived implicit object parameter is neither better nor worse than any standard conversion sequence, but still better than user-defined or ellipsis conversion sequences. Such a change would disambiguate this case in favor of the call operator.
A common source of function objects whose call operators could be static but are not are lambdas without any capture. Had we been able to declare the call operator static when lambdas were originally introduced in the language, we would surely have had a lambda such as:
desugar into:
struct __unique {
static constexpr auto operator()() { return 4; };
using P = int();
constexpr operator P*() { return operator(); }
};
__unique four{};
Rather than desugaring to a type that has a non-static call operator along with a conversion function that has to return some other function.
However, we can’t simply change such lambdas because this could break code. There exists code that takes a template parameter of callable type and does decltype(&F::operator())
, expecting the resulting type to be a pointer to member type (which is the only thing it can be right now). If we change captureless lambdas to have a static call operator implicitly, all such code would break for captureless lambdas. Additionally, this would be a language ABI break. While lambdas shouldn’t show up in your ABI anyway, we can’t with confidence state that such code doesn’t exist nor that such code deserves to be broken.
Instead, we propose that this can be opt-in: a lambda is allowed to be declared static
, which will then cause the call operator (or call operator template) of the lambda to be a static member function rather than a non-static member function:
We then also need to ensure that a lambda cannot be declared static
if it is declared mutable
(an inherently non-static property) or has any capture (as that would be fairly pointless, since you could not access any of that capture).
Consider the following, assuming a version of less
that uses a static call operator:
template <typename T>
struct less {
static constexpr auto operator()(T const& x, T const& y) -> bool {
return x < y;
};
};
std::function f = less<int>{};
This will not compile with this change, because std::function
’s deduction guides only work with either function pointers (which does not apply) or class types whose call operator is a non-static member function. These will need to be extended to support call operators with function type (as they would for [P0847R6] anyway).
This idea was previously referenced in [EWG88], which reads:
In c++std-core-14770, Dos Reis suggests that operator[]
and operator()
should both be allowed to be static. In addition to that, he suggests that both should allow multiple parameters. It’s well known that there’s a possibility that this breaks existing code (foo[1,2]
is valid, the thing in brackets is a comma-expression) but there are possibilities to fix such cases (by requiring parens if a comma-expression is desired). EWG should discuss whether such unification is to be strived for.
Discussed in Rapperswil 2014. EWG points out that there are more issues to consider here, in terms of other operators, motivations, connections with captureless lambdas, who knows what else, so an analysis paper is requested.
There is a separate paper proposing multi-argument subscripting [P2128R3] already, with preexisting code such as foo[1, 2]
already having been deprecated.
The language changes have been implemented in EDG.
Change 7.5.5.1 [expr.prim.lambda.general]/3:
3 In the decl-specifier-seq of the lambda-declarator, each decl-specifier shall be one of
mutable
,static
,constexpr
, orconsteval
. The decl-specifier-seq shall not contain bothmutable
andstatic
. If the decl-specifier-seq containsstatic
, there shall be no lambda-capture.
Change 7.5.5.2 [expr.prim.lambda.closure]/4:
4 The function call operator or operator template is a static member function or static member function template ([class.static.mfct]) if the lambda-expression’s parameter-declaration-clause is followed by
static
. Otherwise, it is a non-static member function or member function template ([class.mfct.non-static]) that is declaredconst
([class.mfct.non-static]) if and only if the lambda-expression’s parameter-declaration-clause is not followed bymutable
. It is neither virtual nor declaredvolatile
. Any noexcept-specifier specified on a lambda-expression applies to the corresponding function call operator or operator template. An attribute-specifier-seq in a lambda-declarator appertains to the type of the corresponding function call operator or operator template. The function call operator or any given operator template specialization is aconstexpr
function if either the corresponding lambda-expression’s parameter-declaration-clause is followed byconstexpr
, or it satisfies the requirements for aconstexpr
function.
Add a note to 7.5.5.2 [expr.prim.lambda.closure]/7 and /10 indicating that we could just return the call operator. The wording as-is specifies the behavior of the return here, and returning the call operator already would be allowed, so no wording change is necessary. But the note would be helpful:
7 The closure type for a non-generic lambda-expression with no lambda-capture whose constraints (if any) are satisfied has a conversion function to pointer to function with C++ language linkage having the same parameter and return types as the closure type’s function call operator. The conversion is to “pointer to
noexcept
function” if the function call operator has a non-throwing exception specification. The value returned by this conversion function is the address of a functionF
that, when invoked, has the same effect as invoking the closure type’s function call operator on a default-constructed instance of the closure type.F
is a constexpr function if the function call operator is a constexpr function and is an immediate function if the function call operator is an immediate function. [Note: if the function call operator is a static member function, the conversion function may return the address of the function call operator. -end note]10 The value returned by any given specialization of this conversion function template is the address of a function
F
that, when invoked, has the same effect as invoking the generic lambda’s corresponding function call operator template specialization on a default-constructed instance of the closure type. F is a constexpr function if the corresponding specialization is a constexpr function and F is an immediate function if the function call operator template specialization is an immediate function. [Note: if the function call operator template is a static member function template, the conversion function may return the address of a specialization of the function call operator template. -end note]
Change 12.2.4.1 [over.match.best.general]/1 to drop the static member exception and remove the bullets and the footnote:
1 Define ICSi(
F
) asfollows:
- (1.1)
IfF
is a static member function, ICS1(F
) is defined such that ICS1(F
) is neither better nor worse than ICS1(G
) for any functionG
, and, symmetrically, ICS1(G
) is neither better nor worse than ICS1(F
);117 otherwise,- (1.2)
let ICSi(the implicit conversion sequence that converts the ith argument in the list to the type of the ith parameter of viable functionF
) denoteF
. [over.best.ics] defines the implicit conversion sequences and [over.ics.rank] defines what it means for one implicit conversion sequence to be a better conversion sequence or worse conversion sequence than another.
Add to 12.2.4.2.1 [over.best.ics.general] a way to compare this static member function case:
* When the parameter is the implicit object parameter of a static member function, the implicit conversion sequence is a standard conversion sequence that is neither better nor worse than any other standard conversion sequence.
Change 12.4
[over.oper] paragraph 6 and introduce bullets to clarify the parsing. static void operator()() { }
is a valid function call operator that has no parameters with this proposal, so needs to be clear that the “has at least one parameter” part refers to the non-member function part of the clause.
6 An operator function shall either
- (6.1) be a
non-staticmember function or- (6.2) be a non-member function that has at least one parameter whose type is a class, a reference to a class, an enumeration, or a reference to an enumeration.
It is not possible to change the precedence, grouping, or number of operands of operators. The meaning of the operators
=
, (unary)&
, and,
(comma), predefined for each type, can be changed for specific class and enumeration types by defining operator functions that implement these operators. Operator functions are inherited in the same manner as other base class functions.
Change 12.4.4 [over.call] paragraph 1:
1 A function call operator function is a function named
operator()
that is anon-staticmember function with an arbitrary number of parameters.
Change the deduction guide for function
in 20.14.17.3.2
[func.wrap.func.con]/14-15:
14 Constraints:
&F::operator()
is well-formed when treated as an unevaluated operand anddecltype(&F::operator())
is either of the formR(G::*)(A...) cv &opt noexceptopt
for a class typeG
or of the formR(*)(A...) noexceptopt
.15 Remarks: The deduced type is
function<R(A...)>
.
Change the deduction guide for packaged_task
in 32.9.10.2
[futures.task.members]/7-8 in the same way (it’s nearly the same wording today):
7 Constraints:
&F::operator()
is well-formed when treated as an unevaluated operand anddecltype(&F::operator())
is either of the formR(G::*)(A...) cv &opt noexceptopt
for a class typeG
or of the formR(*)(A...) noexceptopt
.8 Remarks: The deduced type is
packaged_task<R(A...)>
.
[EWG88] Gabriel Dos Reis. [tiny] Uniform handling of operator[] and operator().
https://wg21.link/ewg88
[P0847R6] Barry Revzin, Gašper Ažman, Sy Brand, Ben Deane. 2021-01-15. Deducing this.
https://wg21.link/p0847r6
[P1169R0] Barry Revzin, Casey Carter. 2018-10-07. static operator().
https://wg21.link/p1169r0
[P2128R3] Corentin Jabot, Isabella Muerte, Daisy Hollman, Christian Trott, Mark Hoemmen. 2021-02-15. Multidimensional subscript operator.
https://wg21.link/p2128r3